package services

import (
	"context"
	"encoding/json"
	"log"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/bus"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
)

// Workload results travel back over the bus for the same reason commands travel
// out over it: the pod serving the HTTP request and the pod holding the agent's
// stream are two different processes, and a map in one cannot be read by the
// other.
//
// Await MUST be called before the command is dispatched, or a fast agent
// answers into a channel nobody is listening on yet. See stepresults.go.

type workloadResultRegistry struct{}

var WorkloadResults = &workloadResultRegistry{}

// Await subscribes to a command's result channel for a log snapshot.
func (r *workloadResultRegistry) Await(commandID string) (<-chan *pb.WorkloadLogsResult, func()) {
	out := make(chan *pb.WorkloadLogsResult, 1)

	ctx, cancel := context.WithCancel(context.Background())
	raw, unsub, err := bus.Subscribe(ctx, bus.ResultChannel+commandID)
	if err != nil {
		log.Printf("workload results: subscribe for %s: %v", commandID, err)
		cancel()
		close(out)
		return out, func() {}
	}

	go func() {
		defer close(out)
		select {
		case <-ctx.Done():
			return
		case b, ok := <-raw:
			if !ok {
				return
			}
			var res pb.WorkloadLogsResult
			if err := json.Unmarshal(b, &res); err != nil {
				log.Printf("workload results: undecodable result for %s: %v", commandID, err)
				return
			}
			out <- &res
		}
	}()

	return out, func() {
		cancel()
		unsub()
	}
}

// AwaitCommand subscribes to a command's result channel for a plain
// CommandResult, which is what a control action answers with.
//
// A control action reuses CommandResult rather than growing a message of its
// own: start, stop and restart succeed or fail, and that is exactly what
// CommandResult already says.
func (r *workloadResultRegistry) AwaitCommand(commandID string) (<-chan *pb.CommandResult, func()) {
	out := make(chan *pb.CommandResult, 1)

	ctx, cancel := context.WithCancel(context.Background())
	raw, unsub, err := bus.Subscribe(ctx, bus.ResultChannel+commandID)
	if err != nil {
		log.Printf("workload results: subscribe for %s: %v", commandID, err)
		cancel()
		close(out)
		return out, func() {}
	}

	go func() {
		defer close(out)
		select {
		case <-ctx.Done():
			return
		case b, ok := <-raw:
			if !ok {
				return
			}
			var res pb.CommandResult
			if err := json.Unmarshal(b, &res); err != nil {
				log.Printf("workload results: undecodable command result for %s: %v", commandID, err)
				return
			}
			out <- &res
		}
	}()

	return out, func() {
		cancel()
		unsub()
	}
}

// Deliver publishes a log result received from an agent. Called on the pod
// holding that agent's stream, which is not usually the pod waiting for it.
func (r *workloadResultRegistry) Deliver(res *pb.WorkloadLogsResult) {
	if res == nil || res.CommandId == "" {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), dispatchAckTimeout)
	defer cancel()
	if _, err := bus.Publish(ctx, bus.ResultChannel+res.CommandId, res); err != nil {
		log.Printf("workload results: publish for %s: %v", res.CommandId, err)
	}
}

// DeliverCommand republishes a CommandResult onto the bus so a waiting pod can
// see it. Publishing with no subscriber is a no-op, so this is safe to call for
// every CommandResult rather than only the ones somebody is waiting on.
func (r *workloadResultRegistry) DeliverCommand(res *pb.CommandResult) {
	if res == nil || res.CommandId == "" {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), dispatchAckTimeout)
	defer cancel()
	if _, err := bus.Publish(ctx, bus.ResultChannel+res.CommandId, res); err != nil {
		log.Printf("workload results: publish command result for %s: %v", res.CommandId, err)
	}
}
