package emitter

import (
	"fmt"
	"strconv"
	"strings"

	"git.cloudinit.dev/coreci/orca/internal/jobspec"
)

// UpdatePlan is the computed update sequence for a Service (P03). It is
// a PLAN, not an execution — the transactional execution lands in
// v0.10-P10. Each step describes a discrete action the executor takes:
// start a set of allocs (Action="start"), wait for them to become
// healthy (WaitForHealthy=true), or cutover from old to new
// (Action="cutover" for blue-green). The Allocs field carries
// placeholder alloc names of the form "<spec.Name>-<index>" where
// index is 1-based (the scheduler assigns the real alloc-id at submit
// time; P03 uses spec.Name as a placeholder per the socket layer
// contract — see SocketEmitter).
type UpdatePlan struct {
	Steps []UpdateStep
}

// UpdateStep is a single step in an UpdatePlan. Action is one of
// "start", "wait", "cutover", "promote". Allocs is the list of
// placeholder alloc names the step applies to. WaitForHealthy is true
// when the executor must wait for the allocs in this step to pass
// their health check before proceeding to the next step (driven by
// min_healthy_time / healthy_deadline on the spec, which the executor
// — not the plan — enforces).
type UpdateStep struct {
	Action         string
	Allocs         []string
	WaitForHealthy bool
}

// maxParallelFor returns the effective max_parallel for the spec,
// defaulting to 1 when unset (0) and clamping to count (the validator
// already rejects out-of-range values; this is a defensive clamp for
// direct callers that bypass the validator).
func maxParallelFor(spec *jobspec.WorkloadSpec) int {
	if spec.Update == nil {
		return 1
	}
	if spec.Update.MaxParallel < 1 {
		return 1
	}
	if spec.Count > 0 && spec.Update.MaxParallel > spec.Count {
		return spec.Count
	}
	return spec.Update.MaxParallel
}

// allocName returns the placeholder alloc name for index i (1-based).
// The real alloc-id is assigned by the scheduler at submit time; P03
// uses spec.Name as the placeholder per the socket-layer contract.
func allocName(spec *jobspec.WorkloadSpec, i int) string {
	return fmt.Sprintf("%s-%d", spec.Name, i)
}

// allAllocs returns the placeholder alloc names for the full count of
// the spec (1..count).
func allAllocs(spec *jobspec.WorkloadSpec) []string {
	out := make([]string, 0, spec.Count)
	for i := 1; i <= spec.Count; i++ {
		out = append(out, allocName(spec, i))
	}
	return out
}

// canaryCount returns the integer canary count for the spec. The
// canary field accepts an integer count or a percentage ("<n>%"). For
// a percentage, the count is ceil(count * n / 100) with a minimum of 1
// when n > 0 (a 10% canary of a 3-replica service is 1 alloc, not 0).
// When the canary field is empty, the default is 1 (a single canary
// alloc — the smallest meaningful canary).
func canaryCount(spec *jobspec.WorkloadSpec) int {
	if spec.Update == nil {
		return 1
	}
	c := strings.TrimSpace(spec.Update.Canary)
	if c == "" {
		return 1
	}
	if strings.HasSuffix(c, "%") {
		n, err := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(c, "%")))
		if err != nil || n <= 0 {
			return 1
		}
		allocs := spec.Count * n / 100
		if allocs < 1 {
			allocs = 1
		}
		return allocs
	}
	n, err := strconv.Atoi(c)
	if err != nil || n < 1 {
		return 1
	}
	if spec.Count > 0 && n > spec.Count {
		return spec.Count
	}
	return n
}

// RenderUpdatePlan computes the rolling/canary/blue-green update
// sequence for a Service spec. Returns an *UpdatePlan describing the
// steps; the actual transactional execution lands in v0.10-P10.
//
// The three strategies:
//
//   - rolling: allocs are started in batches of max_parallel. Each
//     batch waits for healthy before the next batch starts. This is
//     the simplest strategy and the default for stateless services.
//
//   - canary: a single canary alloc (or N per the canary field) is
//     started first and waits for healthy. After the canary is
//     healthy, the plan emits a "promote" step (manual or auto per
//     auto_promote); the remaining allocs are then started in
//     max_parallel batches.
//
//   - blue-green: all new allocs are started in parallel (a single
//     "start" step with the full count). After they are healthy, a
//     "cutover" step swaps traffic from the old allocs to the new
//     ones. The old allocs are then stopped (the stop is implicit in
//     the cutover step for the plan; v0.10-P10 makes it explicit).
//
// Returns an error if the spec is nil, the update block is nil, or
// the strategy is unknown (the validator should have caught these,
// but RenderUpdatePlan is defensive — emitters are called from
// render paths that may bypass the schema validator).
func RenderUpdatePlan(spec *jobspec.WorkloadSpec) (*UpdatePlan, error) {
	if spec == nil {
		return nil, fmt.Errorf("emitter/update: spec is nil")
	}
	if spec.Update == nil {
		return nil, fmt.Errorf("emitter/update: update block is nil")
	}
	if spec.Count < 1 {
		return nil, fmt.Errorf("emitter/update: count must be ≥ 1, got %d", spec.Count)
	}
	switch spec.Update.Strategy {
	case "rolling":
		return renderRollingPlan(spec), nil
	case "canary":
		return renderCanaryPlan(spec), nil
	case "blue-green":
		return renderBlueGreenPlan(spec), nil
	default:
		return nil, fmt.Errorf("emitter/update: unknown strategy %q (want rolling, canary, or blue-green)", spec.Update.Strategy)
	}
}

// renderRollingPlan emits the rolling-update plan: allocs in batches
// of max_parallel, each batch waiting for healthy before the next.
func renderRollingPlan(spec *jobspec.WorkloadSpec) *UpdatePlan {
	plan := &UpdatePlan{}
	batch := maxParallelFor(spec)
	allocs := allAllocs(spec)
	for i := 0; i < len(allocs); i += batch {
		end := i + batch
		if end > len(allocs) {
			end = len(allocs)
		}
		plan.Steps = append(plan.Steps, UpdateStep{
			Action:         "start",
			Allocs:         allocs[i:end],
			WaitForHealthy: true,
		})
	}
	return plan
}

// renderCanaryPlan emits the canary-update plan: a canary batch first
// (size per the canary field, default 1), a "promote" step, then the
// remaining allocs in max_parallel batches.
func renderCanaryPlan(spec *jobspec.WorkloadSpec) *UpdatePlan {
	plan := &UpdatePlan{}
	allocs := allAllocs(spec)
	canary := canaryCount(spec)
	if canary > len(allocs) {
		canary = len(allocs)
	}
	if canary < 1 {
		canary = 1
	}
	// Step 1: start the canary alloc(s) and wait for healthy.
	plan.Steps = append(plan.Steps, UpdateStep{
		Action:         "start",
		Allocs:         allocs[:canary],
		WaitForHealthy: true,
	})
	// Step 2: promote (manual or auto per auto_promote).
	plan.Steps = append(plan.Steps, UpdateStep{
		Action: "promote",
		Allocs: allocs[:canary],
	})
	// Step 3+: remaining allocs in max_parallel batches.
	batch := maxParallelFor(spec)
	remaining := allocs[canary:]
	for i := 0; i < len(remaining); i += batch {
		end := i + batch
		if end > len(remaining) {
			end = len(remaining)
		}
		plan.Steps = append(plan.Steps, UpdateStep{
			Action:         "start",
			Allocs:         remaining[i:end],
			WaitForHealthy: true,
		})
	}
	return plan
}

// renderBlueGreenPlan emits the blue-green update plan: all new allocs
// start in parallel, wait for healthy, then cutover (swap traffic).
func renderBlueGreenPlan(spec *jobspec.WorkloadSpec) *UpdatePlan {
	plan := &UpdatePlan{}
	allocs := allAllocs(spec)
	// Step 1: start ALL new allocs in parallel (blue-green does not
	// batch — the new fleet stands up alongside the old).
	plan.Steps = append(plan.Steps, UpdateStep{
		Action:         "start",
		Allocs:         allocs,
		WaitForHealthy: true,
	})
	// Step 2: cutover — swap traffic from old to new. The old allocs
	// are stopped implicitly as part of the cutover (v0.10-P10 makes
	// the stop explicit in the transactional plane).
	plan.Steps = append(plan.Steps, UpdateStep{
		Action:         "cutover",
		Allocs:         allocs,
		WaitForHealthy: false,
	})
	return plan
}
