package schema

import (
	"fmt"
	"slices"
	"strings"
	"time"
)

// MigrationLoopPlan models migration-loop.json.
//
// The input file the loop driver reads. Produced by prepare; consumed by the
// service loop.
type MigrationLoopPlan struct {
	Runtime  *MigrationLoopRuntime  `json:"runtime,omitempty"`
	Vmselect *MigrationLoopVmselect `json:"vmselect,omitempty"`
	Tenants  []MigrationLoopTenant  `json:"tenants"`
}

// MigrationLoopRuntime carries shared structured inputs used by typed actions.
type MigrationLoopRuntime struct {
	Region       string                    `json:"region"`
	Vmcluster    string                    `json:"vmcluster"`
	Vminsert     string                    `json:"vminsert"`
	Window       MigrationLoopWindow       `json:"window"`
	Verification MigrationLoopVerification `json:"verification"`
}

// MigrationLoopWindow is the manifest coverage and shard interval.
type MigrationLoopWindow struct {
	Start        string `json:"start"`
	End          string `json:"end"`
	StepInterval string `json:"stepInterval"`
}

// MigrationLoopVerification carries the verify policy.
type MigrationLoopVerification struct {
	TimeoutSeconds int     `json:"timeoutSeconds"`
	Tolerance      float64 `json:"tolerance"`
}

// MigrationLoopVmselect carries the three vmselect URL patterns the export
// probe, migrate, and verify paths need.
//
// Written by prepare; omitted entries fall back to the consumer's "recompile
// required" error. Pointer so consumers can distinguish "block absent" from
// "block present but a field is empty".
type MigrationLoopVmselect struct {
	Origin    string `json:"origin"`    // main cluster vmselect
	Migration string `json:"migration"` // migration vmselect — export probe and vmctl source
	Candidate string `json:"candidate"` // candidate vmselect
}

// MigrationLoopTenant is one tenant entry in migration-loop.json.
type MigrationLoopTenant struct {
	Name        string             `json:"name"`
	TenantsFile string             `json:"tenantsFile"`
	StatusFile  string             `json:"statusFile"`
	LogFile     string             `json:"logFile"`
	Cmds        []MigrationLoopCmd `json:"cmds"`
}

// MigrationLoopCmd is one step in migration-loop.json.
type MigrationLoopCmd struct {
	Step   string               `json:"step"`
	Action *MigrationLoopAction `json:"action,omitempty"`
}

// MigrationLoopActionKind selects the in-process executor for a loop step.
type MigrationLoopActionKind string

const (
	ActionManifest MigrationLoopActionKind = "manifest"
	ActionMigrate  MigrationLoopActionKind = "migrate"
	ActionVerify   MigrationLoopActionKind = "verify"
)

// LoopStepBinding pairs one loop step name with the action kind it carries.
type LoopStepBinding struct {
	Step string
	Kind MigrationLoopActionKind
}

// loopStepBindings is the canonical mapping of migration-loop step names to the
// action kind each one carries.
//
// Single source of truth shared by plan rendering, migration validation,
// and server jobs. Adding a loop step means adding one entry here plus a
// payload case in plan.buildLoopAction.
var loopStepBindings = []LoopStepBinding{
	{"manifest", ActionManifest},
	{"migrate", ActionMigrate},
	{"verify", ActionVerify},
}

// KindForLoopStep returns the action kind bound to the loop step name.
//
// Returns ok=false if name is not a known migration-loop step.
// migration.ValidateStep uses it to enforce that a step's name and action
// kind agree.
func KindForLoopStep(step string) (MigrationLoopActionKind, bool) {
	for _, b := range loopStepBindings {
		if b.Step == step {
			return b.Kind, true
		}
	}
	return "", false
}

// LoopSteps returns the canonical loop step bindings in execution order.
//
// Plan rendering builds each tenant's loop commands from this list; the
// loop-plan loader validates persisted plans against the names.
func LoopSteps() []LoopStepBinding {
	return slices.Clone(loopStepBindings)
}

// loopStepNames returns the canonical loop step names in execution order.
func loopStepNames() []string {
	names := make([]string, 0, len(loopStepBindings))
	for _, b := range loopStepBindings {
		names = append(names, b.Step)
	}
	return names
}

// ValidateLoopStepSequence reports whether steps is exactly the required
// migration-loop step sequence — complete, unique, ordered. The loop-plan
// loader calls it so a shortened or reordered persisted plan cannot run to
// completion.
func ValidateLoopStepSequence(steps []string) error {
	required := loopStepNames()
	if slices.Equal(steps, required) {
		return nil
	}
	return fmt.Errorf("steps must be exactly %s in order, got %s",
		strings.Join(required, " -> "), strings.Join(steps, " -> "))
}

// MigrationLoopAction is the executable protocol for one loop step.
//
// Fields not used by a kind remain empty; shared inputs live in
// MigrationLoopRuntime.
type MigrationLoopAction struct {
	Kind           MigrationLoopActionKind `json:"kind"`
	TenantsFile    string                  `json:"tenantsFile,omitempty"`
	Manifest       string                  `json:"manifest,omitempty"`
	RateLimitBytes *int                    `json:"rateLimitBytes,omitempty"`
	Concurrency    *int                    `json:"concurrency,omitempty"`
	StepInterval   string                  `json:"stepInterval,omitempty"`
}

// ValidateTenantURLs rejects non-empty URL templates that lack the
// {tenant} placeholder.
//
// RenderURL returns such templates unchanged, so every tenant would
// silently hit the same address — one destination for all migrations, one
// verify view for all tenants.
func (p *MigrationLoopPlan) ValidateTenantURLs() error {
	check := func(field, url string) error {
		if url != "" && !strings.Contains(url, "{tenant}") {
			return fmt.Errorf("%s URL template must contain {tenant}, got %q", field, url)
		}
		return nil
	}
	if p.Runtime != nil {
		if err := check("vminsert", p.Runtime.Vminsert); err != nil {
			return err
		}
	}
	if p.Vmselect != nil {
		for _, f := range []struct{ field, url string }{
			{"vmselect.origin", p.Vmselect.Origin},
			{"vmselect.migration", p.Vmselect.Migration},
			{"vmselect.candidate", p.Vmselect.Candidate},
		} {
			if err := check(f.field, f.url); err != nil {
				return err
			}
		}
	}
	return nil
}

// VmctlTune is the vmctl section of tune.json. Nil fields inherit the plan's
// migrate action values; a non-nil field replaces it at the next shard
// boundary.
type VmctlTune struct {
	RateLimitBytes *int   `json:"rateLimitBytes,omitempty"`
	Concurrency    *int   `json:"concurrency,omitempty"`
	UpdatedAt      string `json:"updatedAt,omitempty"`
}

// IsEmpty reports whether the section carries no override.
func (t *VmctlTune) IsEmpty() bool {
	return t.RateLimitBytes == nil && t.Concurrency == nil
}

// Validate rejects knob values vmctl cannot honour.
func (t *VmctlTune) Validate() error {
	if t.Concurrency != nil && *t.Concurrency < 1 {
		return fmt.Errorf("concurrency must be >= 1, got %d", *t.Concurrency)
	}
	if t.RateLimitBytes != nil && *t.RateLimitBytes < 0 {
		return fmt.Errorf("rateLimitBytes must be >= 0, got %d", *t.RateLimitBytes)
	}
	return nil
}

// MergedOver returns the effective knobs: t's non-nil fields over the plan
// pair, carrying t's UpdatedAt stamp.
func (t VmctlTune) MergedOver(rateLimitBytes, concurrency *int) VmctlTune {
	if t.RateLimitBytes != nil {
		rateLimitBytes = t.RateLimitBytes
	}
	if t.Concurrency != nil {
		concurrency = t.Concurrency
	}
	return VmctlTune{RateLimitBytes: rateLimitBytes, Concurrency: concurrency, UpdatedAt: t.UpdatedAt}
}

// TuneTarget overrides one temporary vmselect's image or sizing; nil fields
// inherit the compiled plan's value.
type TuneTarget struct {
	Image     *string            `json:"image,omitempty"`
	Replicas  *int               `json:"replicas,omitempty"`
	Resources *CompiledResources `json:"resources,omitempty"`
}

// MaxVmselectReplicas bounds a temporary vmselect's replicas everywhere a
// value enters (plan.json, tune bodies, the compiled-plan snapshot): one or
// two per target in practice, and anything larger than an int32 would wrap
// the Deployment's replicas field.
const MaxVmselectReplicas = 10

// Validate rejects override values the deployment cannot honour.
func (t *TuneTarget) Validate() error {
	if t.Image != nil && *t.Image == "" {
		return fmt.Errorf("image must not be empty")
	}
	if t.Replicas != nil && (*t.Replicas < 1 || *t.Replicas > MaxVmselectReplicas) {
		return fmt.Errorf("replicas must be between 1 and %d, got %d", MaxVmselectReplicas, *t.Replicas)
	}
	if t.Resources == nil {
		return nil
	}
	for _, field := range []struct {
		label string
		value *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 field.value != nil && *field.value == "" {
			return fmt.Errorf("%s must not be empty", field.label)
		}
	}
	return nil
}

// VmselectTune is the vmselect section of tune.json: per-target overrides
// replaced wholesale. Nil targets inherit the plan.
type VmselectTune struct {
	Migration *TuneTarget `json:"migration,omitempty"`
	Candidate *TuneTarget `json:"candidate,omitempty"`
	UpdatedAt string      `json:"updatedAt,omitempty"`
}

// IsEmpty reports whether no target carries an override.
func (t *VmselectTune) IsEmpty() bool {
	return t.Migration == nil && t.Candidate == nil
}

// Validate rejects overrides the deployment cannot honour.
func (t *VmselectTune) Validate() error {
	for _, target := range []struct {
		name   string
		values *TuneTarget
	}{
		{"migration", t.Migration},
		{"candidate", t.Candidate},
	} {
		if target.values == nil {
			continue
		}
		if err := target.values.Validate(); err != nil {
			return fmt.Errorf("vmselect.%s: %w", target.name, err)
		}
	}
	return nil
}

// TuneDoc is the per-migration runtime knob override document (tune.json):
// one section per knob family, each replaced wholesale by POST /tune.
type TuneDoc struct {
	Vmctl    VmctlTune    `json:"vmctl"`
	Vmselect VmselectTune `json:"vmselect"`
}

// IsEmpty reports whether no section carries an override.
func (d *TuneDoc) IsEmpty() bool {
	return d.Vmctl.IsEmpty() && d.Vmselect.IsEmpty()
}

// VmctlTuneSet is the vmctl section of a POST /tune body: the knob
// overrides only. UpdatedAt is server-owned and stamped on write.
type VmctlTuneSet struct {
	RateLimitBytes *int `json:"rateLimitBytes,omitempty"`
	Concurrency    *int `json:"concurrency,omitempty"`
}

// Section converts the request payload to the unstamped doc section.
func (s VmctlTuneSet) Section() VmctlTune {
	return VmctlTune{RateLimitBytes: s.RateLimitBytes, Concurrency: s.Concurrency}
}

// VmselectTuneSet is the vmselect section of a POST /tune body.
type VmselectTuneSet struct {
	Migration *TuneTarget `json:"migration,omitempty"`
	Candidate *TuneTarget `json:"candidate,omitempty"`
}

// Section converts the request payload to the unstamped doc section.
func (s VmselectTuneSet) Section() VmselectTune {
	return VmselectTune{Migration: s.Migration, Candidate: s.Candidate}
}

// TuneSetRequest is the POST /tune body: a present section replaces its
// override wholesale, an empty section removes it, an absent section is
// untouched.
type TuneSetRequest struct {
	Vmctl    *VmctlTuneSet    `json:"vmctl,omitempty"`
	Vmselect *VmselectTuneSet `json:"vmselect,omitempty"`
}

// TuneView is the GET /tune response: per-section effective values (the
// compiled plan with tune.json overrides applied).
type TuneView struct {
	Vmctl    VmctlTune        `json:"vmctl"`
	Vmselect VmselectTuneView `json:"vmselect"`
}

// VmselectTuneView is the effective vmselect section: one entry per
// temporary vmselect.
type VmselectTuneView struct {
	Migration TuneTargetView `json:"migration"`
	Candidate TuneTargetView `json:"candidate"`
	UpdatedAt string         `json:"updatedAt,omitempty"`
}

// TuneTargetView is one temporary vmselect's effective deployment spec.
type TuneTargetView struct {
	Name      string         `json:"name"`
	URL       string         `json:"url"`
	Image     string         `json:"image"`
	Replicas  int            `json:"replicas"`
	Resources TuneQuantities `json:"resources"`
}

// TuneQuantities is the concrete resources block of a TuneTargetView.
type TuneQuantities struct {
	Requests TuneQuantity `json:"requests"`
	Limits   TuneQuantity `json:"limits"`
}

// TuneQuantity is one of {requests, limits} with concrete values.
type TuneQuantity struct {
	CPU    string `json:"cpu"`
	Memory string `json:"memory"`
}

// MigrationLoopStatus models migration-loop-status.json.
//
// The runtime state file the loop driver writes atomically after each
// transition.
type MigrationLoopStatus struct {
	UpdatedAt string                      `json:"updatedAt"`
	Tenants   []MigrationLoopTenantStatus `json:"tenants"`
}

// MigrationLoopTenantStatus is one tenant entry in migration-loop-status.json.
type MigrationLoopTenantStatus struct {
	Name        string                    `json:"name"`
	Completed   bool                      `json:"completed"`
	Failed      bool                      `json:"failed"`
	CurrentStep string                    `json:"currentStep"`
	StartedAt   *time.Time                `json:"startedAt,omitempty"`
	FinishedAt  *time.Time                `json:"finishedAt,omitempty"`
	Steps       []MigrationLoopStepStatus `json:"steps"`
}

// MigrationLoopStepStatus tracks per-step progress in migration-loop-status.json.
type MigrationLoopStepStatus struct {
	Step         string              `json:"step"`
	Status       string              `json:"status"`
	Attempts     int                 `json:"attempts"`
	StartedAt    *time.Time          `json:"startedAt,omitempty"`
	FinishedAt   *time.Time          `json:"finishedAt,omitempty"`
	LastExitCode *int                `json:"lastExitCode,omitempty"`
	LastError    *MigrationLoopError `json:"lastError,omitempty"`
}

// MigrationLoopError is the per-step error record (category, message).
type MigrationLoopError struct {
	Category string `json:"category"`
	Message  string `json:"message"`
}

// MigrationLoopPending is one tenant's not-yet-completed steps in plan order.
type MigrationLoopPending struct {
	Name  string                     `json:"name"`
	Steps []MigrationLoopPendingStep `json:"steps"`
}

// MigrationLoopPendingStep is one pending step with its full action spec.
//
// Cmds carries the complete external command lines (migrate only) and is
// omitted until the manifest the action references has been built.
type MigrationLoopPendingStep struct {
	Step   string               `json:"step"`
	Action *MigrationLoopAction `json:"action,omitempty"`
	Cmds   []string             `json:"cmds,omitempty"`
}
