package migration

import (
	"context"
	"errors"
	"fmt"
	"io"

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

// stepRunner runs a single tenant step once. Transient failures are retried
// at the vmquery call level, not here; re-running the loop resumes a failed
// step.
//
// The failure classification is persisted to step.LastError so operators can
// see why a step failed.
type stepRunner struct {
	loop      *Loop
	tenant    *schema.MigrationLoopTenantStatus
	step      *schema.MigrationLoopStepStatus
	action    *schema.MigrationLoopAction
	actionLog *tenantActionLog // borrowed from Loop.runTenant; outlives Execute, not beyond runTenant
}

// Execute runs the step.
//
// Returns nil on success; otherwise an error wrapping the classified failure
// message.
func (s *stepRunner) Execute(ctx context.Context) error {
	if err := s.runOnce(ctx); err != nil {
		return fmt.Errorf("step %s: %w", s.step.Step, err)
	}
	return nil
}

// runOnce executes the step and records its outcome.
//
// Returns nil on success; otherwise an error carrying the classified message
// or a dump/IO fault.
func (s *stepRunner) runOnce(ctx context.Context) error {
	if s.tenant.StartedAt == nil {
		s.tenant.StartedAt = new(status.UTCNow())
	}
	s.tenant.CurrentStep = s.step.Step
	s.step.Status = StatusRunning
	s.step.Attempts++
	s.step.StartedAt = new(status.UTCNow())
	s.step.FinishedAt = nil
	s.step.LastExitCode = nil
	s.step.LastError = nil
	if err := s.loop.dump(); err != nil {
		return err
	}

	printer.Printf(s.loop.output.Progress, "tenant=%s step=%s attempt=%d status=%s",
		s.tenant.Name, s.step.Step, s.step.Attempts, StatusRunning)
	writeStepHeader(s.actionLog, s.tenant.Name, s.step.Step, s.step.Attempts)

	runErr := s.runAction(ctx)

	exitCode := 0
	if runErr != nil {
		exitCode = int(exitcode.Classify(runErr))
	}
	s.step.LastExitCode = new(exitCode)
	s.step.FinishedAt = new(status.UTCNow())

	if runErr == nil {
		s.step.Status = StatusCompleted
		s.step.LastError = nil
		if err := s.loop.dump(); err != nil {
			return err
		}
		writeStepResult(s.actionLog, StatusCompleted, 0, nil)
		printer.Printf(s.loop.output.Progress, "tenant=%s step=%s status=%s",
			s.tenant.Name, s.step.Step, StatusCompleted)
		return nil
	}

	cat := classifyError(runErr)
	s.step.Status = StatusFailed
	s.step.LastError = cat
	if err := s.loop.dump(); err != nil {
		return err
	}
	writeStepResult(s.actionLog, StatusFailed, *s.step.LastExitCode, cat)
	printer.Printf(s.loop.output.Progress, "tenant=%s step=%s status=%s actionLog=%s",
		s.tenant.Name, s.step.Step, StatusFailed, s.actionLog.Path())
	return errors.New(cat.Message)
}

func (s *stepRunner) runAction(ctx context.Context) error {
	return s.loop.runAction(ctx, s.loop.plan, s.loop.planDir, s.action, s.loop.vmctlRunner, ActionOutput{
		Progress: s.loop.output.Progress,
		Log:      &timestampWriter{w: s.actionLog},
	})
}

// timestampWriter prepends "[<rfc3339>] " to each action output write.
//
// Preserves the historical per-tenant log format.
type timestampWriter struct {
	w io.Writer
}

func (t *timestampWriter) Write(p []byte) (int, error) {
	now := status.FormatTime(status.UTCNow())
	entry := fmt.Sprintf("[%s] %s", now, p)
	_, err := io.WriteString(t.w, entry)
	if err != nil {
		return 0, err
	}
	return len(p), nil
}

// classifyError preserves the typed retry contract used by the domain packages.
//
// Unclassified errors default to permanent; execution must opt in explicitly
// by matching retry.ErrTransient.
func classifyError(err error) *schema.MigrationLoopError {
	if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
		return &schema.MigrationLoopError{
			Category: CategoryInfrastructure,
			Message:  err.Error(),
		}
	}
	if errors.Is(err, retry.ErrTransient) {
		return &schema.MigrationLoopError{
			Category: CategoryTransient,
			Message:  err.Error(),
		}
	}
	return &schema.MigrationLoopError{
		Category: CategoryPermanent,
		Message:  err.Error(),
	}
}
