package plan

import (
	"errors"
	"fmt"
	"slices"

	"github.com/prometheus/common/model"

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

// validatePlan runs semantic checks on the parsed RawPlan.
//
// JSON tags handle type coercion; this catches missing required strings (zero
// values), the source/target replica invariant, and the
// step-names-must-be-valid rule. Collects ALL missing-field errors into
// one report via errors.Join so the operator doesn't fix-then-rerun N times.
func validatePlan(raw *schema.RawPlan) error {
	var missing []error
	requireString := func(name, v string) {
		if v == "" {
			missing = append(missing, fmt.Errorf("plan.json field %s is required (currently null). Fill it in and rerun", name))
		}
	}

	// Replica invariant — checked separately because the error message shape
	// differs from the per-field "currently null" pattern.
	if raw.SourceReplicaCount <= 0 {
		return fmt.Errorf("plan.json field sourceReplicaCount must be a positive integer; got %d", raw.SourceReplicaCount)
	}
	if raw.TargetReplicaCount <= 0 || raw.TargetReplicaCount >= raw.SourceReplicaCount {
		return fmt.Errorf("plan.json requires 0 < targetReplicaCount < sourceReplicaCount; got source=%d, target=%d",
			raw.SourceReplicaCount, raw.TargetReplicaCount)
	}

	requireString("namespace", raw.Namespace)
	requireString("vmselects.image", raw.Vmselects.Image)
	requireVmselectTarget := func(target string, t schema.RawVmselectTarget) {
		if t.Replicas == nil || *t.Replicas < 1 || *t.Replicas > schema.MaxVmselectReplicas {
			missing = append(missing, fmt.Errorf("plan.json field vmselects.%s.replicas must be between 1 and %d", target, schema.MaxVmselectReplicas))
		}
		for _, quant := range []struct {
			field string
			v     *string
		}{
			{"resources.requests.cpu", t.Resources.Requests.CPU},
			{"resources.requests.memory", t.Resources.Requests.Memory},
			{"resources.limits.cpu", t.Resources.Limits.CPU},
			{"resources.limits.memory", t.Resources.Limits.Memory},
		} {
			if quant.v == nil || *quant.v == "" {
				missing = append(missing, fmt.Errorf("plan.json field vmselects.%s.%s is required (currently null). Fill it in and rerun", target, quant.field))
			}
		}
	}
	requireVmselectTarget("migration", raw.Vmselects.Migration)
	requireVmselectTarget("candidate", raw.Vmselects.Candidate)
	requireString("region", raw.Region)
	requireString("retentionWindow.start", raw.RetentionWindow.Start)
	requireString("retentionWindow.end", raw.RetentionWindow.End)
	requireString("shardStepInterval", raw.ShardStepInterval)

	if err := errors.Join(missing...); err != nil {
		return err
	}

	// Time fields parse with the same parsers the runtime uses; a malformed
	// value would otherwise surface only when the manifest action first runs.
	start, err := status.ParseTime(raw.RetentionWindow.Start)
	if err != nil {
		return fmt.Errorf("plan.json field retentionWindow.start must be an RFC3339 timestamp: %w", err)
	}
	end, err := status.ParseTime(raw.RetentionWindow.End)
	if err != nil {
		return fmt.Errorf("plan.json field retentionWindow.end must be an RFC3339 timestamp: %w", err)
	}
	if !end.After(start) {
		return fmt.Errorf("plan.json requires retentionWindow.end after start; got start=%s end=%s",
			raw.RetentionWindow.Start, raw.RetentionWindow.End)
	}
	step, err := model.ParseDuration(raw.ShardStepInterval)
	if err != nil {
		return fmt.Errorf("plan.json field shardStepInterval must be a Prometheus duration (e.g. 6h): %w", err)
	}
	if step <= 0 {
		return fmt.Errorf("plan.json field shardStepInterval must be positive; got %s", raw.ShardStepInterval)
	}

	for _, stage := range []struct {
		name  string
		items []schema.Step
	}{
		{StagePrepare, raw.Steps.Prepare.Items},
		{StageMigration, raw.Steps.Migration.Items},
		{StageDecommission, raw.Steps.Decommission.Items},
	} {
		for i, s := range stage.items {
			name := s.Name
			if name == "" {
				name = "<unnamed>"
			}
			if !slices.Contains(ValidStepNames, name) {
				if _, ok := schema.KindForLoopStep(name); ok {
					return fmt.Errorf("plan.json steps.%s.items[%d]: step '%s' is loop-internal, not a plan step; regenerate the plan", stage.name, i, name)
				}
				return fmt.Errorf("plan.json steps.%s.items[%d] references unknown step '%s'", stage.name, i, name)
			}
		}
	}

	return nil
}
