package services

import (
	"context"
	"errors"
	"fmt"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/workflowsched"
	"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"
)

// ErrDefaultStep is returned when a caller tries to edit or delete a step that
// came from the image's default library. Those rows are re-seeded from disk on
// every boot, so an edit would be silently reverted and a delete would come
// back — refusing is honest about who owns them.
var ErrDefaultStep = errors.New("this step ships with Vantage and cannot be edited or deleted; duplicate it to make your own copy")

func isDefaultStep(ctx context.Context, instanceID, stepID string) (bool, error) {
	var s models.WorkflowStep
	err := db.Col("workflow_steps").FindOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}).Decode(&s)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return s.Source == "default", nil
}

func wfCtx() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), 10*time.Second)
}

func EnsureWorkflowIndexes() error {
	ctx, cancel := wfCtx()
	defer cancel()
	if _, err := db.Col("workflow_steps").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "step_id", Value: 1}}, Options: options.Index().SetUnique(true),
	}); err != nil {
		return err
	}

	if err := db.Col("workflow_steps").Indexes().DropOne(ctx, "slug_1"); err != nil && !isIndexNotFound(err) {
		return err
	}
	if _, err := db.Col("workflow_steps").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "slug", Value: 1}},
		Options: options.Index().SetUnique(true).
			SetPartialFilterExpression(bson.M{"source": "default"}),
	}); err != nil {
		return err
	}
	if _, err := db.Col("workflows").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "workflow_id", Value: 1}}, Options: options.Index().SetUnique(true),
	}); err != nil {
		return err
	}
	if _, err := db.Col("workflows").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "next_run_at", Value: 1}},
	}); err != nil {
		return err
	}
	if err := EnsureLogIndexes(); err != nil {
		return err
	}
	_, err := db.Col("workflow_runs").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "run_id", Value: 1}}, Options: options.Index().SetUnique(true),
	})
	return err
}

func ListSteps(instanceID string) ([]models.WorkflowStep, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	cur, err := db.Col("workflow_steps").Find(ctx, bson.M{"instance_id": instanceID},
		options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)
	steps := []models.WorkflowStep{}
	if err := cur.All(ctx, &steps); err != nil {
		return nil, err
	}
	return steps, nil
}

func StepUsageCounts(instanceID string) (map[string]int, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	cur, err := db.Col("workflows").Find(ctx, bson.M{"instance_id": instanceID})
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)
	var wfs []models.Workflow
	if err := cur.All(ctx, &wfs); err != nil {
		return nil, err
	}
	counts := map[string]int{}
	for _, w := range wfs {
		seen := map[string]bool{}
		for _, ref := range w.Steps {
			if ref.StepID == "" || seen[ref.StepID] {
				continue
			}
			seen[ref.StepID] = true
			counts[ref.StepID]++
		}
	}
	return counts, nil
}

func CreateStep(instanceID string, s models.WorkflowStep) (*models.WorkflowStep, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	s.InstanceID = instanceID
	s.StepID = uuid.New().String()
	s.CreatedAt = time.Now()
	s.UpdatedAt = s.CreatedAt
	s.DeclaredOutputs = DeriveOutputs(s.Script)
	if s.Source == "" {
		s.Source = "user"
	}
	if s.SecretRefs == nil {
		s.SecretRefs = []string{}
	}
	if s.DeclaredInputs == nil {
		s.DeclaredInputs = []models.InputParam{}
	}
	if _, err := db.Col("workflow_steps").InsertOne(ctx, s); err != nil {
		return nil, err
	}
	return &s, nil
}

func UpdateStep(instanceID, stepID string, s models.WorkflowStep) error {
	ctx, cancel := wfCtx()
	defer cancel()
	if def, err := isDefaultStep(ctx, instanceID, stepID); err != nil {
		return err
	} else if def {
		return ErrDefaultStep
	}
	_, err := db.Col("workflow_steps").UpdateOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}, bson.M{"$set": bson.M{
		"name":             s.Name,
		"description":      s.Description,
		"interpreter":      s.Interpreter,
		"script":           s.Script,
		"declared_outputs": DeriveOutputs(s.Script),
		"declared_inputs":  s.DeclaredInputs,
		"secret_refs":      s.SecretRefs,
		"updated_at":       time.Now(),
	}})
	return err
}

func DeleteStep(instanceID, stepID string) error {
	ctx, cancel := wfCtx()
	defer cancel()
	if def, err := isDefaultStep(ctx, instanceID, stepID); err != nil {
		return err
	} else if def {
		return ErrDefaultStep
	}
	if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}); err != nil {
		return err
	}

	cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID, "instance_id": instanceID})
	if err != nil {
		return err
	}
	defer cur.Close(ctx)
	var wfs []models.Workflow
	if err := cur.All(ctx, &wfs); err != nil {
		return err
	}
	for _, w := range wfs {
		kept := make([]models.WorkflowStepRef, 0, len(w.Steps))
		for _, ref := range w.Steps {
			if ref.StepID == stepID {
				continue
			}
			kept = append(kept, ref)
		}
		for i := range kept {
			kept[i].Order = i
		}
		if _, err := db.Col("workflows").UpdateOne(ctx,
			bson.M{"workflow_id": w.WorkflowID},
			bson.M{"$set": bson.M{"steps": kept, "updated_at": time.Now()}},
		); err != nil {
			return err
		}
	}
	return nil
}

func getStep(ctx context.Context, instanceID, stepID string) (*models.WorkflowStep, error) {
	var s models.WorkflowStep
	err := db.Col("workflow_steps").FindOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}).Decode(&s)
	if err == mongo.ErrNoDocuments {
		return nil, fmt.Errorf("step %s not found", stepID)
	}
	return &s, err
}

func ListWorkflows(instanceID string) ([]models.Workflow, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	cur, err := db.Col("workflows").Find(ctx, bson.M{"instance_id": instanceID},
		options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
	if err != nil {
		return nil, err
	}
	defer cur.Close(ctx)
	wfs := []models.Workflow{}
	if err := cur.All(ctx, &wfs); err != nil {
		return nil, err
	}
	return wfs, nil
}

func GetWorkflow(instanceID, id string) (*models.Workflow, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	var w models.Workflow
	err := db.Col("workflows").FindOne(ctx, bson.M{"workflow_id": id, "instance_id": instanceID}).Decode(&w)
	if err == mongo.ErrNoDocuments {
		return nil, fmt.Errorf("workflow not found")
	}
	return &w, err
}

func CreateWorkflow(instanceID string, w models.Workflow) (*models.Workflow, error) {
	ctx, cancel := wfCtx()
	defer cancel()
	w.InstanceID = instanceID
	w.WorkflowID = uuid.New().String()
	w.CreatedAt = time.Now()
	w.UpdatedAt = w.CreatedAt
	if w.TargetServerIDs == nil {
		w.TargetServerIDs = []string{}
	}
	if w.Steps == nil {
		w.Steps = []models.WorkflowStepRef{}
	}
	if err := ValidateWorkflow(w); err != nil {
		return nil, err
	}
	if err := ValidateTags(w.TargetTags); err != nil {
		return nil, err
	}
	if err := validateTargetServers(instanceID, w.TargetServerIDs); err != nil {
		return nil, err
	}
	normalizeInlineSteps(&w)
	if _, err := db.Col("workflows").InsertOne(ctx, w); err != nil {
		return nil, err
	}
	return &w, nil
}

func UpdateWorkflow(instanceID, id string, w models.Workflow) error {
	ctx, cancel := wfCtx()
	defer cancel()
	if err := ValidateWorkflow(w); err != nil {
		return err
	}
	if err := ValidateTags(w.TargetTags); err != nil {
		return err
	}
	if err := validateTargetServers(instanceID, w.TargetServerIDs); err != nil {
		return err
	}
	normalizeInlineSteps(&w)
	_, err := db.Col("workflows").UpdateOne(ctx, bson.M{"workflow_id": id, "instance_id": instanceID}, bson.M{"$set": bson.M{
		"name":              w.Name,
		"target_server_ids": w.TargetServerIDs,
		"target_tags":       w.TargetTags,
		"steps":             w.Steps,
		"updated_at":        time.Now(),
	}})
	return err
}

func validateTargetServers(instanceID string, serverIDs []string) error {
	for _, sid := range serverIDs {
		if _, err := GetServer(instanceID, sid); err != nil {
			return fmt.Errorf("target server %s not found", sid)
		}
	}
	return nil
}

func normalizeInlineSteps(w *models.Workflow) {
	for i := range w.Steps {
		in := w.Steps[i].Inline
		if in == nil {
			continue
		}
		in.DeclaredOutputs = DeriveOutputs(in.Script)
		in.StepID = ""
		in.Slug = ""
		in.Source = ""
		in.CreatedAt = time.Time{}
		in.UpdatedAt = time.Time{}
		if in.SecretRefs == nil {
			in.SecretRefs = []string{}
		}
		if in.DeclaredInputs == nil {
			in.DeclaredInputs = []models.InputParam{}
		}
	}
}

func DeleteWorkflow(instanceID, id string) error {
	ctx, cancel := wfCtx()
	defer cancel()
	_, err := db.Col("workflows").DeleteOne(ctx, bson.M{"workflow_id": id, "instance_id": instanceID})
	return err
}

// SetSchedule validates and stores a workflow's schedule, computing the first
// occurrence. next_run_at is persisted rather than held in memory: a leader
// handover between computing an occurrence and firing it would otherwise lose
// it or fire it twice.
//
// Passing s == nil, or a disabled schedule, clears next_run_at so the
// scheduler's query stops matching the document at all.
func SetSchedule(instanceID, workflowID string, s *models.Schedule) (*time.Time, error) {
	ctx, cancel := wfCtx()
	defer cancel()

	set := bson.M{"schedule": s, "updated_at": time.Now()}
	unset := bson.M{}

	var next *time.Time
	if s != nil && s.Enabled {
		at, err := workflowsched.NextOccurrence(s.Cron, s.TZ, time.Now())
		if err != nil {
			return nil, err
		}
		next = &at
		set["next_run_at"] = at
	} else {
		unset["next_run_at"] = ""
	}

	update := bson.M{"$set": set}
	if len(unset) > 0 {
		update["$unset"] = unset
	}

	res, err := db.Col("workflows").UpdateOne(ctx,
		bson.M{"workflow_id": workflowID, "instance_id": instanceID}, update)
	if err != nil {
		return nil, err
	}
	if res.MatchedCount == 0 {
		return nil, mongo.ErrNoDocuments
	}
	return next, nil
}
