package agentops

import (
	"context"
	"fmt"
	"log/slog"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

const (
	defaultOperationPollInterval = 5 * time.Second
	maxItemErrorLen              = 4096
)

// OperationExecutor runs one operation item. The worker calls it in strict
// index order; a non-nil error marks the item failed and the worker continues.
// A nil error marks it applied.
type OperationExecutor interface {
	ExecuteItem(ctx context.Context, op *Operation, item OperationItem) (result model.JSONDocument, err error)
}

// OperationTerminalObserver projects the authoritative terminal operation
// result before the worker commits the operation's terminal status.
type OperationTerminalObserver interface {
	ObserveOperationTerminal(context.Context, OperationTerminalInput) error
}

// OperationTerminalInput is the narrow, ordered terminal projection surface.
type OperationTerminalInput struct {
	Operation      Operation
	TerminalStatus string
	Items          []OperationItem
}

// OperationWorker polls for queued operations and executes their items in order.
// On context cancel it exits immediately, leaving the operation running for
// manual check — it never fabricates failed.
type OperationWorker struct {
	store    *Store
	executor OperationExecutor
	interval time.Duration
	logger   *slog.Logger
	observer OperationTerminalObserver
	// workspaceGroupID limits production workspace-profile workers to
	// operations currently routed to their group. Empty preserves global mode.
	workspaceGroupID string
	// processOnce is an internal seam for testing Run scheduling without a
	// database clock or a second store abstraction.
	processOnce func(context.Context)
	// completeOperation is a focused test seam for the final store CAS.
	completeOperation func(context.Context, int64, string) error
}

type OperationWorkerOption func(*OperationWorker)

// WithOperationWorkspaceGroupID scopes operation claims to the current
// routable placements for one workspace profile group. Empty keeps global
// all-profile behavior.
func WithOperationWorkspaceGroupID(workspaceGroupID string) OperationWorkerOption {
	return func(worker *OperationWorker) {
		worker.workspaceGroupID = strings.TrimSpace(workspaceGroupID)
	}
}

func NewOperationWorker(
	store *Store,
	executor OperationExecutor,
	observer OperationTerminalObserver,
	interval time.Duration,
	logger *slog.Logger,
	options ...OperationWorkerOption,
) (*OperationWorker, error) {
	if store == nil || executor == nil || observer == nil {
		return nil, fmt.Errorf("operation worker dependencies are incomplete")
	}
	if interval <= 0 {
		interval = defaultOperationPollInterval
	}
	worker := &OperationWorker{
		store: store, executor: executor, observer: observer,
		interval: interval, logger: logger,
	}
	for _, option := range options {
		if option != nil {
			option(worker)
		}
	}
	worker.processOnce = worker.ProcessOnce
	worker.completeOperation = store.CompleteOperation
	return worker, nil
}

func (w *OperationWorker) Run(ctx context.Context) {
	if ctx.Err() != nil {
		return
	}
	w.processOnce(ctx)

	ticker := time.NewTicker(w.interval)
	defer ticker.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			w.processOnce(ctx)
		}
	}
}

// ProcessOnce claims one eligible operation, executes queued work in strict
// order, reloads all items, projects terminal on the existing processing card,
// then commits the operation terminal status. Finalization retries never invoke
// the executor again.
func (w *OperationWorker) ProcessOnce(ctx context.Context) {
	op, err := w.store.ClaimNextOperation(ctx, w.workspaceGroupID)
	if err != nil {
		w.warn(ctx, "claim", err)
		return
	}
	if op == nil {
		return
	}

	items, err := w.store.LoadItems(ctx, op.ID, true)
	if err != nil {
		w.warn(ctx, "load items", err)
		return // keep running
	}
	if len(items) == 0 {
		// A finalization-ready running operation has no pending items. Its full
		// terminal item set is validated below.
	} else {
		for _, item := range items {
			if ctx.Err() != nil {
				return // cancel → exit, keep running
			}
			result, execErr := w.executor.ExecuteItem(ctx, op, item)
			if ctx.Err() != nil {
				return // cancel during exec → exit, keep running
			}
			if execErr != nil {
				if err := w.store.CompleteItem(ctx, op.ID, item.ItemIndex, "failed", result, truncateItemError(execErr.Error())); err != nil {
					w.warn(ctx, "complete item", err)
					return
				}
			} else {
				if err := w.store.CompleteItem(ctx, op.ID, item.ItemIndex, "applied", result, ""); err != nil {
					w.warn(ctx, "complete item", err)
					return
				}
			}
		}
	}

	if ctx.Err() != nil {
		return // cancel before terminal → keep running
	}
	allItems, err := w.store.LoadItems(ctx, op.ID, false)
	if err != nil {
		w.warn(ctx, "reload items", err)
		return
	}
	if len(allItems) == 0 {
		w.warn(ctx, "process", fmt.Errorf("operation %d has no items", op.ID))
		return
	}
	applied, failed := 0, 0
	for _, item := range allItems {
		switch item.Status {
		case "applied":
			applied++
		case "failed":
			failed++
		default:
			w.warn(ctx, "process", fmt.Errorf("operation %d item %d remains %s", op.ID, item.ItemIndex, item.Status))
			return
		}
	}
	terminal := aggregateOperationStatus(applied, failed)
	if err := w.observer.ObserveOperationTerminal(ctx, OperationTerminalInput{
		Operation: *op, TerminalStatus: terminal, Items: allItems,
	}); err != nil {
		w.warn(ctx, "observe terminal", err)
		return
	}
	if err := w.completeOperation(ctx, op.ID, terminal); err != nil {
		w.warn(ctx, "complete operation", err)
	}
}

func aggregateOperationStatus(applied, failed int) string {
	switch {
	case failed == 0:
		return "applied"
	case applied == 0:
		return "failed"
	default:
		return "partial"
	}
}

func truncateItemError(s string) string {
	s = strings.ToValidUTF8(s, "")
	if len(s) <= maxItemErrorLen {
		return s
	}
	end := maxItemErrorLen
	for end > 0 && !utf8.RuneStart(s[end]) {
		end--
	}
	return s[:end]
}

func (w *OperationWorker) warn(ctx context.Context, msg string, err error) {
	if w.logger != nil {
		w.logger.WarnContext(ctx, "operation worker: "+msg, "error", err)
	}
}
