package controlplane

import (
	"fmt"
	"log"
	"time"
)

// JobResult tracks the execution of a control-plane operation.
type JobResult struct {
	Operation string
	Status    string // "completed" | "failed"
	Duration  time.Duration
	Steps     []StepResult
	Error     error
}

// StepResult tracks an individual step within an operation.
type StepResult struct {
	Name     string
	Duration time.Duration
	Error    error
}

// trackStep runs fn, records timing and any error, and appends to the job.
// trackStep is best-effort: a step error is logged and stored on the job but
// does NOT halt the caller. Use trackStepFatal for steps that MUST succeed
// before the caller can safely proceed (e.g. cutover migrations whose failure
// would let a new runtime contract execute against unmigrated data).
func (j *JobResult) trackStep(name string, fn func() error) {
	start := time.Now()
	err := fn()
	step := StepResult{
		Name:     name,
		Duration: time.Since(start),
		Error:    err,
	}
	j.Steps = append(j.Steps, step)
	if err != nil {
		log.Printf("   ⚠️  %s failed: %v (%.1fs)", name, err, step.Duration.Seconds())
	} else {
		log.Printf("   ✅ %s (%.1fs)", name, step.Duration.Seconds())
	}
}

// trackStepFatal runs fn, records the StepResult, and returns the step error
// so the caller can abort the operation. Use this for cutover migrations or
// any step whose failure must block downstream work — best-effort logging
// (trackStep) is not enough when the step gates a runtime-contract change
// that would otherwise corrupt data or silently drop side effects.
//
// On failure the JobResult.Status is also flipped to "failed" and the error
// is recorded on the job so Summary() reflects the abort accurately.
func (j *JobResult) trackStepFatal(name string, fn func() error) error {
	start := time.Now()
	err := fn()
	step := StepResult{
		Name:     name,
		Duration: time.Since(start),
		Error:    err,
	}
	j.Steps = append(j.Steps, step)
	if err != nil {
		log.Printf("   ❌ %s failed (fatal): %v (%.1fs)", name, err, step.Duration.Seconds())
		j.Status = "failed"
		j.Error = err
		return fmt.Errorf("%s: %w", name, err)
	}
	log.Printf("   ✅ %s (%.1fs)", name, step.Duration.Seconds())
	return nil
}

// FailedStepNames returns the names of steps that recorded an error, filtered to
// those in `critical`. Used by the ApplyAll marker path to fail closed when a
// convergence-critical best-effort repair step silently failed, so the completed
// marker is not stamped on an unconverged control plane.
func (j *JobResult) FailedStepNames(critical map[string]bool) []string {
	var failed []string
	for _, s := range j.Steps {
		if s.Error != nil && critical[s.Name] {
			failed = append(failed, s.Name)
		}
	}
	return failed
}

// Summary returns a human-readable summary of the job.
func (j *JobResult) Summary() string {
	failed := 0
	for _, s := range j.Steps {
		if s.Error != nil {
			failed++
		}
	}
	return fmt.Sprintf("operation=%s status=%s duration=%.1fs steps=%d failed=%d",
		j.Operation, j.Status, j.Duration.Seconds(), len(j.Steps), failed)
}
