package services

import (
	"context"
	"fmt"
	"strings"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/grpc/pb"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

// workloadResultTimeout bounds how long an API request waits for an agent to
// answer a control action or a log read. It is well above the agent's own
// 90-second control timeout and 60-second log timeout, so a slow-but-working
// agent reports its real error rather than being cut off by this side.
const workloadResultTimeout = 120 * time.Second

// MaxWorkloadLogLines mirrors the agent's own cap. It is declared again here
// rather than imported: agent/ is a separate module with an internal/ tree, so
// the two cannot share a constant. Change one, change the other — the same
// shape of hazard as the mirrored token blocks in the web apps.
const MaxWorkloadLogLines = 500

// workloadProtectedMarker is the text the agent's ErrProtected carries. The
// refusal crosses the wire as a string, so this is how the control plane knows
// a 409 is owed rather than a 502.
const workloadProtectedMarker = "workload is protected"

// IsWorkloadProtected reports whether an agent refused because the target is
// protected — the agent's own guard, which is the boundary. Nothing failed, so
// the API answers 409 rather than an error status.
func IsWorkloadProtected(err error) bool {
	return err != nil && strings.Contains(err.Error(), workloadProtectedMarker)
}

func HasWorkloadHash(instanceID, serverID, hash string) (bool, error) {
	err := db.Col("server_workloads").FindOne(context.Background(), bson.M{
		"instance_id": instanceID,
		"server_id":   serverID,
		"hash":        hash,
	}, options.FindOne().SetProjection(bson.M{"_id": 1})).Err()
	if err == mongo.ErrNoDocuments {
		return false, nil
	}
	return err == nil, err
}

// StoreWorkloads replaces a server's workload list.
func StoreWorkloads(instanceID, serverID, hash string, wls []models.Workload,
	dockerOK bool, dockerErr string, systemdOK bool, systemdErr string) error {

	_, err := db.Col("server_workloads").UpdateOne(context.Background(),
		bson.M{"instance_id": instanceID, "server_id": serverID},
		bson.M{"$set": bson.M{
			"hash":          hash,
			"workloads":     wls,
			"collected_at":  time.Now(),
			"docker_ok":     dockerOK,
			"docker_error":  dockerErr,
			"systemd_ok":    systemdOK,
			"systemd_error": systemdErr,
		}},
		options.UpdateOne().SetUpsert(true),
	)
	return err
}

func GetWorkloads(instanceID, serverID string) (*models.ServerWorkloads, error) {
	var sw models.ServerWorkloads
	err := db.Col("server_workloads").FindOne(context.Background(), bson.M{
		"instance_id": instanceID,
		"server_id":   serverID,
	}).Decode(&sw)
	if err == mongo.ErrNoDocuments {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &sw, nil
}

type WorkloadHit struct {
	ServerID string          `json:"server_id"`
	Workload models.Workload `json:"workload"`
}

// SearchWorkloads answers "which servers run image X" — the reason the snapshot
// is stored rather than fetched on demand and discarded.
func SearchWorkloads(instanceID, image, stack, state string) ([]WorkloadHit, error) {
	ctx := context.Background()

	filter := bson.M{"instance_id": instanceID}
	if image != "" {
		filter["workloads.image"] = image
	}

	cur, err := db.Col("server_workloads").Find(ctx, filter)
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)

	var docs []models.ServerWorkloads
	if err := cur.All(ctx, &docs); err != nil {
		return nil, err
	}

	hits := []WorkloadHit{}
	for _, d := range docs {
		for _, w := range d.Workloads {
			if image != "" && w.Image != image {
				continue
			}
			if stack != "" && w.Stack != stack {
				continue
			}
			if state != "" && w.State != state {
				continue
			}
			hits = append(hits, WorkloadHit{ServerID: d.ServerID, Workload: w})
		}
	}
	return hits, nil
}

// DispatchRefreshWorkloads asks an agent to report immediately. It returns as
// soon as the owning pod acks; the caller refetches the stored document.
//
// The refresh carries nothing back on purpose: the agent answers through the
// normal ReportWorkloads RPC, so server_workloads has exactly one writer.
func DispatchRefreshWorkloads(serverID string) error {
	return Dispatcher.dispatch(serverID, &pb.ServerCommand{
		CommandId:        uuid.New().String(),
		RefreshWorkloads: &pb.RefreshWorkloadsCmd{},
	})
}

// DispatchControlWorkload runs a control action and waits for the agent's
// CommandResult.
//
// Await is called BEFORE dispatch. Reversing those two lines introduces a race
// that only shows under load, on a fast agent answering into a channel nobody
// has joined yet.
func DispatchControlWorkload(serverID, kind, id, action string) error {
	commandID := uuid.New().String()

	results, done := WorkloadResults.AwaitCommand(commandID)
	defer done()

	if err := Dispatcher.dispatch(serverID, &pb.ServerCommand{
		CommandId:       commandID,
		ControlWorkload: &pb.ControlWorkloadCmd{Kind: kind, Id: id, Action: action},
	}); err != nil {
		return err
	}

	select {
	case res, ok := <-results:
		if !ok || res == nil {
			return fmt.Errorf("no result from agent for %s %s", action, id)
		}
		if !res.Success {
			return fmt.Errorf("%s", res.Message)
		}
		return nil
	case <-time.After(workloadResultTimeout):
		return fmt.Errorf("timed out waiting for the agent to %s %s", action, id)
	}
}

// DispatchWorkloadLogs fetches a bounded log snapshot.
//
// Await is called BEFORE dispatch, for the same reason as above.
func DispatchWorkloadLogs(serverID, kind, id string, tail int) (string, bool, error) {
	commandID := uuid.New().String()

	results, done := WorkloadResults.Await(commandID)
	defer done()

	if err := Dispatcher.dispatch(serverID, &pb.ServerCommand{
		CommandId:    commandID,
		WorkloadLogs: &pb.WorkloadLogsCmd{Kind: kind, Id: id, Tail: int32(tail)},
	}); err != nil {
		return "", false, err
	}

	select {
	case res, ok := <-results:
		if !ok || res == nil {
			return "", false, fmt.Errorf("no log result from agent for %s", id)
		}
		if res.Error != "" {
			return "", false, fmt.Errorf("%s", res.Error)
		}
		return res.Text, res.Truncated, nil
	case <-time.After(workloadResultTimeout):
		return "", false, fmt.Errorf("timed out waiting for logs for %s", id)
	}
}
