package migration

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/tidbcloud/observability/vm-migration/internal/exitcode"
	"github.com/tidbcloud/observability/vm-migration/internal/printer"
	"github.com/tidbcloud/observability/vm-migration/internal/schema"
	"github.com/tidbcloud/observability/vm-migration/internal/status"
)

// Step status literals stored under MigrationLoopStepStatus.Status.
//
// These cross the wire to migration-loop-status.json; literals are part of the
// stable schema.
const (
	StatusPending   = "pending"
	StatusRunning   = "running"
	StatusFailed    = "failed"
	StatusCompleted = "completed"
	StepDone        = "done"
)

// MigrationLoopError.Category values cross the wire as part of the stable
// status JSON schema.
const (
	// CategoryPermanent: typed execution reported a data/usage error.
	CategoryPermanent = "permanent"
	// CategoryTransient: typed execution matched retry.ErrTransient.
	CategoryTransient = "transient"
	// CategoryInfrastructure: execution was cancelled or context expired.
	CategoryInfrastructure = "infrastructure"
)

// Loop drives one migration-loop plan.
//
// Construct via NewLoop; the same Loop may be reused across Run passes.
// Loop is NOT safe for concurrent use.
type Loop struct {
	planDir     string
	loopPath    string
	statusPath  string
	plan        *schema.MigrationLoopPlan
	status      *schema.MigrationLoopStatus
	output      LoopOutput
	warnings    []string
	vmctlRunner VmctlRunner
	runAction   ActionExecutor
}

// ActionExecutor is the in-process dispatch boundary for one typed action.
//
// Production uses RunAction; tests may inject deterministic fakes.
type ActionExecutor func(
	context.Context,
	*schema.MigrationLoopPlan,
	string,
	*schema.MigrationLoopAction,
	VmctlRunner,
	ActionOutput,
) error

// Option customizes a Loop dependency at construction time.
type Option func(*Loop)

// WithActionExecutor replaces typed action dispatch.
//
// Used primarily by service and pipeline tests that need deterministic timing.
func WithActionExecutor(executor ActionExecutor) Option {
	return func(loop *Loop) {
		if executor != nil {
			loop.runAction = executor
		}
	}
}

// NewLoop resolves loopPath, loads plan and existing status, and reconciles the
// runtime status document.
//
// loopPath is absolute or relative to process cwd. Output routes command
// results, operator progress, and optional live action-log observation.
// Every per-tenant artifact anchors on the plan's directory, not the invocation
// cwd. Does not write to disk; the first dump happens when Run executes.
func NewLoop(loopPath string, output LoopOutput, options ...Option) (*Loop, error) {
	absPlan, err := filepath.Abs(loopPath)
	if err != nil {
		return nil, err
	}
	planDir := filepath.Dir(absPlan)
	statusPath := DeriveLoopStatusPath(absPlan)
	plan, existingStatus, err := loadInputs(absPlan, statusPath)
	if err != nil {
		return nil, err
	}
	status, err := reconcileStatus(plan, existingStatus, planDir)
	if err != nil {
		return nil, err
	}
	return newLoop(absPlan, plan, status, output, options...), nil
}

// newLoop wires a Loop from resolved paths and loaded documents. Shared by
// NewLoop and in-package tests that bypass loadInputs/reconcileStatus with
// prebuilt plan and status documents.
func newLoop(absPlan string, plan *schema.MigrationLoopPlan, status *schema.MigrationLoopStatus, output LoopOutput, options ...Option) *Loop {
	loop := &Loop{
		planDir:     filepath.Dir(absPlan),
		loopPath:    absPlan,
		statusPath:  DeriveLoopStatusPath(absPlan),
		plan:        plan,
		status:      status,
		output:      output.normalized(),
		vmctlRunner: NewDefaultVmctlRunner(),
		runAction:   RunAction,
	}
	for _, option := range options {
		option(loop)
	}
	return loop
}

// Run drives one full pass over the plan.
//
// Every tenant that is not yet Completed gets attempted in order. A prior
// Failed state is cleared before resuming from the first incomplete step.
// Step failures mark the tenant Failed and the pass continues so a single
// broken tenant does not block the rest. After the pass, returns
// exitcode.Partial if any tenant failed.
func (l *Loop) Run(ctx context.Context) error {
	l.warnings = nil
	if err := l.dump(); err != nil {
		return err
	}

	var failed []string
	attempted := 0
	for i := range l.status.Tenants {
		t := &l.status.Tenants[i]
		if t.Completed {
			continue
		}
		// A cancelled pass stops here: the remaining tenants keep their
		// recorded state instead of being marked failed by an unwind.
		if ctx.Err() != nil {
			printer.Printf(l.output.Result, "pass cancelled; remaining tenants untouched")
			return exitcode.CancelledFailure(errors.New("pass cancelled"))
		}
		t.Failed = false
		t.FinishedAt = nil
		attempted++
		if err := l.runTenant(ctx, i); err != nil {
			t.Failed = true
			t.FinishedAt = new(status.UTCNow())
			if err := l.dump(); err != nil {
				return err
			}
			failed = append(failed, t.Name)
			printer.Printf(l.output.Progress, "tenant %s failed: %v — continuing to next tenant", t.Name, err)
		}
	}

	if attempted == 0 {
		printer.Printf(l.output.Result, "all-tenants-done")
		return nil
	}
	if len(failed) > 0 {
		printer.Printf(l.output.Result, "failed tenants (%d): %s", len(failed), strings.Join(failed, ", "))
		return exitcode.PartialFailure(fmt.Errorf("%d tenant(s) failed: %s", len(failed), strings.Join(failed, ", ")))
	}
	printer.Printf(l.output.Result, "all-tenants-done")
	return nil
}

// runTenant executes a tenant's steps in order, skipping completed.
func (l *Loop) runTenant(ctx context.Context, idx int) error {
	tenant := &l.status.Tenants[idx]
	tp := l.plan.Tenants[idx]
	actionLog := openTenantActionLog(l.planDir, tp, l.output.ActionLogObserver, l.recordWarning)
	defer actionLog.Close()
	for i := range tenant.Steps {
		step := &tenant.Steps[i]
		planStep := schema.MigrationLoopCmd{}
		if i < len(tp.Cmds) {
			planStep = tp.Cmds[i]
		}
		if step.Status == StatusCompleted {
			continue
		}
		if err := (&stepRunner{
			loop:      l,
			tenant:    tenant,
			step:      step,
			action:    planStep.Action,
			actionLog: actionLog,
		}).Execute(ctx); err != nil {
			return err
		}
	}
	tenant.Completed = true
	tenant.CurrentStep = StepDone
	tenant.FinishedAt = new(status.UTCNow())
	if err := l.dump(); err != nil {
		return err
	}
	return nil
}

// Warnings returns user-visible non-fatal problems encountered by the loop.
func (l *Loop) Warnings() []string {
	return append([]string(nil), l.warnings...)
}

func (l *Loop) recordWarning(message string) {
	l.warnings = append(l.warnings, message)
	printer.Printf(l.output.Progress, "warning: %s", message)
}

// dump flushes the in-memory status to disk atomically.
func (l *Loop) dump() error {
	return dumpStatusAtomic(l.statusPath, l.status)
}

// PendingSteps returns each tenant's not-yet-completed steps in plan order,
// each with its full action spec and, for migrate steps, the complete vmctl
// command lines.
//
// Tenants with nothing pending are omitted.
func (l *Loop) PendingSteps() []schema.MigrationLoopPending {
	out := make([]schema.MigrationLoopPending, 0, len(l.status.Tenants))
	for i, tenant := range l.status.Tenants {
		tp := l.plan.Tenants[i]
		var pending []schema.MigrationLoopPendingStep
		for j, s := range tenant.Steps {
			if s.Status == StatusCompleted || j >= len(tp.Cmds) {
				continue
			}
			cmd := tp.Cmds[j]
			step := schema.MigrationLoopPendingStep{Step: cmd.Step, Action: cmd.Action}
			if cmd.Action != nil && cmd.Action.Kind == schema.ActionMigrate {
				step.Cmds = l.migrateCommands(cmd.Action, tp)
			}
			pending = append(pending, step)
		}
		if len(pending) == 0 {
			continue
		}
		out = append(out, schema.MigrationLoopPending{Name: tenant.Name, Steps: pending})
	}
	return out
}

// migrateCommands renders the full vmctl command for every shard of the
// action's manifest.
//
// The on-disk manifest wins once it exists; otherwise the shard list is
// computed in memory from tp's manifest action and nothing is written.
// Returns nil when a present manifest fails to load.
func (l *Loop) migrateCommands(action *schema.MigrationLoopAction, tp schema.MigrationLoopTenant) []string {
	manifest, err := status.LoadManifest(Resolve(l.planDir, action.Manifest))
	if errors.Is(err, os.ErrNotExist) {
		manifest = l.previewManifest(manifestActionFor(tp))
	}
	if manifest == nil {
		return nil
	}
	var cmds []string
	for _, t := range manifest.Tenants {
		for _, s := range t.Shards {
			cmds = append(cmds, strings.Join(
				vmctlCommand(t, s, action.StepInterval, action.RateLimitBytes, action.Concurrency), " "))
		}
	}
	return cmds
}

// previewManifest returns the in-memory manifest for manifestAction, or nil
// when it cannot be computed.
func (l *Loop) previewManifest(manifestAction *schema.MigrationLoopAction) *schema.Manifest {
	opts, err := manifestOptions(l.plan, l.planDir, manifestAction)
	if err != nil {
		return nil
	}
	manifest, err := ComputeManifest(opts)
	if err != nil {
		return nil
	}
	return manifest
}
