package cluster

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"
)

// Engine runs the lifecycle state machine.
type Engine struct {
	Lifecycle LifecycleKind
	State     *State
	StateDir  string
	Steps     map[StepName]Step
	mu        sync.Mutex // protects all State mutations
	retry     RetryPolicy
	retried   int
}

// EngineOptions configures lifecycle execution semantics.
type EngineOptions struct {
	Retry     RetryPolicy
	Lifecycle LifecycleKind
}

// RetryPolicy provides per-state retry budgets plus an optional total cap.
type RetryPolicy struct {
	Total  int
	ByStep map[StepName]int
}

// NewEngine creates an engine with the given state directory and steps.
// If a state file exists, it loads and resumes; otherwise creates fresh state.
func NewEngine(stateDir, clusterName, sessionID string, steps map[StepName]Step) (*Engine, error) {
	return NewEngineWithOptions(stateDir, clusterName, sessionID, steps, EngineOptions{})
}

// NewEngineWithOptions creates an engine with explicit execution options.
func NewEngineWithOptions(stateDir, clusterName, sessionID string, steps map[StepName]Step, options EngineOptions) (*Engine, error) {
	lifecycle := options.Lifecycle
	if lifecycle == "" {
		lifecycle = LifecycleLegacyV2
	}

	var state *State
	exists, existsErr := StateExists(stateDir)
	if existsErr != nil {
		return nil, fmt.Errorf("check state file: %w", existsErr)
	}
	if exists {
		var err error
		state, err = LoadState(stateDir)
		if err != nil {
			return nil, fmt.Errorf("load existing state: %w", err)
		}
		if state.ClusterName != clusterName {
			return nil, fmt.Errorf("state file cluster %q does not match requested %q", state.ClusterName, clusterName)
		}
		state.Lifecycle = lifecycle
		// Normalize: ensure all expected steps exist and reset failed/interrupted
		normalizeState(state, lifecycle)
	} else {
		state = NewStateForLifecycle(clusterName, sessionID, lifecycle)
	}

	// Validate all required steps are provided
	for _, name := range StepsForLifecycle(lifecycle) {
		if _, ok := steps[name]; !ok {
			return nil, fmt.Errorf("missing step %q", name)
		}
	}

	return &Engine{
		Lifecycle: lifecycle,
		State:     state,
		StateDir:  stateDir,
		Steps:     steps,
		retry:     options.Retry,
	}, nil
}

// Run executes the lifecycle phases in order, respecting SIGINT.
// Returns a timing summary and any error.
func (e *Engine) Run(ctx context.Context) (*TimingSummary, error) {
	releaseLock, err := acquireSessionLock(e.StateDir)
	if err != nil {
		return nil, err
	}
	defer releaseLock()

	ctx, cancel := context.WithCancel(ctx)
	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, os.Interrupt)
	sigDone := make(chan struct{})
	defer func() {
		signal.Stop(sigCh)
		cancel()
		<-sigDone
	}()

	go func() {
		defer close(sigDone)
		select {
		case <-sigCh:
			cancel()
			e.mu.Lock()
			e.State.MarkInterrupted()
			_ = SaveState(e.StateDir, e.State)
			e.mu.Unlock()
		case <-ctx.Done():
		}
	}()

	phases := PlanForLifecycle(e.Lifecycle, e.Steps)

	for _, phase := range phases {
		if ctx.Err() != nil {
			return e.lockedBuildSummary(), ctx.Err()
		}

		// Skip phases where all steps are already done
		if e.lockedPhaseComplete(phase) {
			continue
		}

		if err := e.runPhase(ctx, phase); err != nil {
			_ = e.lockedSaveState()
			return e.lockedBuildSummary(), err
		}

		if err := e.lockedSaveState(); err != nil {
			return e.lockedBuildSummary(), fmt.Errorf("save state: %w", err)
		}
	}

	return e.lockedBuildSummary(), nil
}

func (e *Engine) lockedPhaseComplete(phase Phase) bool {
	e.mu.Lock()
	defer e.mu.Unlock()
	for _, step := range phase.Steps {
		st := e.State.Steps[step.Name()]
		if st.Status != StatusDone && st.Status != StatusSkipped {
			return false
		}
	}
	return true
}

func (e *Engine) lockedSaveState() error {
	e.mu.Lock()
	defer e.mu.Unlock()
	return SaveState(e.StateDir, e.State)
}

func (e *Engine) lockedBuildSummary() *TimingSummary {
	e.mu.Lock()
	defer e.mu.Unlock()
	return e.buildSummary()
}

func (e *Engine) runPhase(ctx context.Context, phase Phase) error {
	if !phase.Parallel || len(phase.Steps) == 1 {
		for _, step := range phase.Steps {
			if err := e.runStep(ctx, step); err != nil {
				return err
			}
		}
		return nil
	}

	return e.runParallel(ctx, phase.Steps)
}

func (e *Engine) runStep(ctx context.Context, step Step) error {
	name := step.Name()
	budget := e.retry.budgetFor(name)

	for attempt := 1; attempt <= budget; attempt++ {
		e.mu.Lock()
		st := e.State.Steps[name]
		if st.Status == StatusDone || st.Status == StatusSkipped {
			e.mu.Unlock()
			return nil
		}
		e.State.MarkRunning(name)
		e.mu.Unlock()

		var err error
		if stateAware, ok := step.(StateAwareStep); ok {
			e.mu.Lock()
			err = stateAware.RunWithState(ctx, e.State)
			e.mu.Unlock()
		} else {
			err = step.Run(ctx)
		}

		baseErr := unwrapStepError(err)
		e.mu.Lock()
		if err == nil {
			e.State.MarkDone(name)
			e.mu.Unlock()
			return nil
		}
		if errors.Is(baseErr, context.Canceled) && ctx.Err() != nil {
			now := time.Now()
			st := e.State.Steps[name]
			st.Status = StatusInterrupted
			st.Error = ""
			st.EndedAt = &now
			e.State.UpdatedAt = now
			e.mu.Unlock()
			return fmt.Errorf("step %s failed: %w", name, baseErr)
		}
		retryable := isTransientError(err) &&
			!errors.Is(baseErr, context.Canceled) &&
			!errors.Is(baseErr, context.DeadlineExceeded) &&
			attempt < budget &&
			(e.retry.Total == 0 || e.retried < e.retry.Total)
		if retryable {
			e.retried++
			st := e.State.Steps[name]
			st.Status = StatusPending
			st.Error = ""
			st.EndedAt = nil
			e.mu.Unlock()
			continue
		}
		e.State.MarkFailed(name, baseErr)
		e.mu.Unlock()
		return fmt.Errorf("step %s failed: %w", name, baseErr)
	}

	return nil
}

func (e *Engine) runParallel(ctx context.Context, steps []Step) error {
	type result struct {
		err error
	}
	toRun := make([]Step, 0, len(steps))
	e.mu.Lock()
	for _, step := range steps {
		st := e.State.Steps[step.Name()]
		if st.Status == StatusDone || st.Status == StatusSkipped {
			continue
		}
		e.State.MarkRunning(step.Name())
		toRun = append(toRun, step)
	}
	e.mu.Unlock()

	results := make([]result, len(toRun))
	var wg sync.WaitGroup
	for i, step := range toRun {
		index := i
		s := step
		wg.Go(func() {
			if stateAware, ok := s.(StateAwareStep); ok {
				e.mu.Lock()
				results[index].err = stateAware.RunWithState(ctx, e.State)
				e.mu.Unlock()
				return
			}
			results[index].err = s.Run(ctx)
		})
	}

	wg.Wait()
	var failures []error
	e.mu.Lock()
	for i, result := range results {
		step := toRun[i]
		if result.err != nil {
			baseErr := unwrapStepError(result.err)
			if errors.Is(baseErr, context.Canceled) && ctx.Err() != nil {
				now := time.Now()
				st := e.State.Steps[step.Name()]
				st.Status = StatusInterrupted
				st.Error = ""
				st.EndedAt = &now
				e.State.UpdatedAt = now
			} else {
				e.State.MarkFailed(step.Name(), baseErr)
			}
			failures = append(failures, fmt.Errorf("step %s failed: %w", step.Name(), baseErr))
			continue
		}
		e.State.MarkDone(step.Name())
	}
	e.mu.Unlock()
	return errors.Join(failures...)
}

// TimingSummary holds the timing breakdown for display.
type TimingSummary struct {
	Status                 string           `json:"status"`
	Message                string           `json:"message"`
	Steps                  []StepTiming     `json:"steps"`
	ReadinessChecks        []ReadinessCheck `json:"readiness_checks,omitempty"`
	Total                  time.Duration    `json:"total"`
	Complete               bool             `json:"complete"`
	InfrastructureComplete bool             `json:"infrastructure_complete"`
}

// StepTiming is a single step's timing info.
type StepTiming struct {
	Name     StepName      `json:"name"`
	Status   StepStatus    `json:"status"`
	Duration time.Duration `json:"duration"`
}

func (e *Engine) buildSummary() *TimingSummary {
	var timings []StepTiming
	for _, name := range StepsForLifecycle(e.Lifecycle) {
		st := e.State.Steps[name]
		d := st.Duration()
		timings = append(timings, StepTiming{
			Name:     name,
			Status:   st.Status,
			Duration: d,
		})
	}
	var total time.Duration
	for _, phase := range PlanForLifecycle(e.Lifecycle, e.Steps) {
		var phaseDuration time.Duration
		for _, step := range phase.Steps {
			phaseDuration = max(phaseDuration, e.State.Steps[step.Name()].Duration())
		}
		total += phaseDuration
	}
	status := "failed"
	message := "infrastructure setup did not complete"
	if e.State.IsComplete() {
		status = "initializing"
		message = "infrastructure is ready; cp9 up is attaching the actionable product-readiness watch"
	}
	return &TimingSummary{
		Status:                 status,
		Message:                message,
		Steps:                  timings,
		ReadinessChecks:        append([]ReadinessCheck(nil), e.State.ReadinessChecks...),
		Total:                  total,
		Complete:               false,
		InfrastructureComplete: e.State.IsComplete(),
	}
}

// FormatText returns a human-readable timing breakdown.
func (s *TimingSummary) FormatText() string {
	var b strings.Builder
	fmt.Fprintf(&b, "Status: %s\n%s\n\n", s.Status, s.Message)

	b.WriteString("Timing breakdown:\n")
	for _, t := range s.Steps {
		icon := statusIcon(t.Status)
		if t.Duration > 0 {
			fmt.Fprintf(&b, "  %s %-20s %s\n", icon, t.Name, t.Duration.Round(time.Millisecond))
		} else {
			fmt.Fprintf(&b, "  %s %-20s %s\n", icon, t.Name, t.Status)
		}
	}
	if len(s.ReadinessChecks) > 0 {
		b.WriteString("\nReadiness checks:\n")
		for _, check := range s.ReadinessChecks {
			if strings.TrimSpace(check.Message) == "" {
				fmt.Fprintf(&b, "  - %s: %s\n", check.Name, check.Status)
				continue
			}
			fmt.Fprintf(&b, "  - %s: %s (%s)\n", check.Name, check.Status, check.Message)
		}
	}
	fmt.Fprintf(&b, "\nTotal: %s", s.Total.Round(time.Millisecond))
	return b.String()
}

// normalizeState ensures all expected steps exist in the state map
// and resets failed/interrupted steps to pending for retry.
func normalizeState(state *State, lifecycle LifecycleKind) {
	if state.Steps == nil {
		state.Steps = make(map[StepName]*StepState)
	}
	for _, name := range StepsForLifecycle(lifecycle) {
		st, ok := state.Steps[name]
		if !ok || st == nil {
			state.Steps[name] = &StepState{Status: StatusPending}
			continue
		}
		if st.Status == StatusFailed || st.Status == StatusInterrupted {
			st.Status = StatusPending
			st.Error = ""
			st.StartedAt = nil
			st.EndedAt = nil
		}
	}
}

func statusIcon(s StepStatus) string {
	switch s {
	case StatusDone:
		return "[ok]"
	case StatusFailed:
		return "[FAIL]"
	case StatusRunning:
		return "[...]"
	case StatusInterrupted:
		return "[INT]"
	case StatusSkipped:
		return "[skip]"
	default:
		return "[ ]"
	}
}

func acquireSessionLock(stateDir string) (func(), error) {
	if err := os.MkdirAll(stateDir, 0o755); err != nil {
		return nil, fmt.Errorf("create state dir for lock: %w", err)
	}

	lockPath := LockPath(stateDir)
	lockFile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("open session lock: %w", err)
	}

	if err := syscall.Flock(int(lockFile.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		_ = lockFile.Close()
		return nil, fmt.Errorf("session %q is already running — release %s or wait for the other cp9 process to finish", stateDir, lockPath)
	}

	return func() {
		_ = syscall.Flock(int(lockFile.Fd()), syscall.LOCK_UN)
		_ = lockFile.Close()
	}, nil
}
