package plan

import (
	"bytes"
	"encoding/json"
	"fmt"
	"path/filepath"
	"strings"
	"text/template"

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

// RenderCompiled produces the JSON text for <stem>-compile.json.
//
// Plan placeholders are resolved against the static plan values.
func RenderCompiled(cfg *Config, stages Stages) (string, error) {
	out, err := buildCompiledDoc(cfg, stages)
	if err != nil {
		return "", err
	}
	data, err := jsonio.MarshalIndent(out)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// buildCompiledDoc assembles the wire-format CompiledDoc from a parsed plan,
// resolving plan placeholders.
//
// Shared by RenderCompiled (string output, used by the golden test + stdout)
// and WriteCompiled (direct jsonio.WriteFile).
func buildCompiledDoc(cfg *Config, stages Stages) (*schema.CompiledDoc, error) {
	ctx := buildFormatCtx(cfg)

	prepareItems, err := renderItems(stages[StagePrepare], ctx)
	if err != nil {
		return nil, err
	}
	migrationItems, err := renderItems(stages[StageMigration], ctx)
	if err != nil {
		return nil, err
	}
	decommissionItems, err := renderItems(stages[StageDecommission], ctx)
	if err != nil {
		return nil, err
	}

	out := &schema.CompiledDoc{
		Namespace: cfg.Namespace,
		Region:    cfg.Region,
		RetentionWindow: schema.CompiledWindow{
			Start: cfg.WindowStart,
			End:   cfg.WindowEnd,
		},
		ShardStepInterval:  cfg.ShardStepInterval,
		SourceReplicaCount: cfg.SourceReplicaCount,
		Steps: schema.CompiledSteps{
			Decommission: schema.CompiledStage{Items: decommissionItems},
			Migration:    schema.CompiledStage{Items: migrationItems},
			Prepare:      schema.CompiledStage{Items: prepareItems},
		},
		TargetReplicaCount: cfg.TargetReplicaCount,
		Tenants:            append([]string(nil), cfg.Tenants...),
		Vmcluster: schema.CompiledVmcluster{
			Name:               schema.VmclusterName,
			VmselectName:       schema.VmselectName,
			VmselectURLPattern: cfg.VmselectURLPattern,
			VminsertName:       schema.VminsertName,
			VminsertURLPattern: cfg.VminsertURLPattern,
		},
		Vmctl: schema.CompiledVmctl{
			Concurrency:    cfg.VmctlConcurrency,
			RateLimitBytes: cfg.VmctlRateLimit,
			StepInterval:   cfg.VmctlStepInterval,
		},
		Vmselects: schema.CompiledVmselects{
			Image:     cfg.VmselectImage,
			Migration: compiledVmselectTarget(schema.VmselectMigrationName, cfg.VmselectMigrationURLPattern, cfg.VmselectMigration),
			Candidate: compiledVmselectTarget(schema.VmselectCandidateName, cfg.VmselectCandidateURLPattern, cfg.VmselectCandidate),
		},
	}
	return out, nil
}

func compiledVmselectTarget(name, urlPattern string, sizing TargetSizing) schema.CompiledVmselectTarget {
	return schema.CompiledVmselectTarget{
		Name:       name,
		URLPattern: urlPattern,
		Replicas:   sizing.Replicas,
		Resources: schema.CompiledResources{
			Requests: schema.CompiledResourceQuant{CPU: sizing.CPURequest, Memory: sizing.MemoryRequest},
			Limits:   schema.CompiledResourceQuant{CPU: sizing.CPULimit, Memory: sizing.MemoryLimit},
		},
	}
}

// BuildMigrationLoop constructs the in-memory MigrationLoopPlan from cfg.
// The step sequence comes from schema.LoopStepNames — the code's loop
// bindings — not from plan.json's migration items, which are operator
// annotations only.
//
// One tenant entry per id in cfg.Tenants: Name keeps the real id, while the
// artifact file names derive from the NormalizeTenantIDs stems. Duplicate
// ids and stem collisions are rejected.
func BuildMigrationLoop(cfg *Config) (*schema.MigrationLoopPlan, error) {
	tenantIDs, stems, err := NormalizeTenantIDs(cfg.Tenants)
	if err != nil {
		return nil, err
	}
	loopSteps := schema.LoopSteps()

	loopPlan := &schema.MigrationLoopPlan{
		Runtime: &schema.MigrationLoopRuntime{
			Region:    cfg.Region,
			Vmcluster: schema.VmclusterName,
			Vminsert:  cfg.VminsertURLPattern,
			Window: schema.MigrationLoopWindow{
				Start:        cfg.WindowStart,
				End:          cfg.WindowEnd,
				StepInterval: cfg.ShardStepInterval,
			},
			Verification: schema.MigrationLoopVerification{
				TimeoutSeconds: 60,
				Tolerance:      0.01,
			},
		},
		Vmselect: &schema.MigrationLoopVmselect{
			Origin:    cfg.VmselectURLPattern,
			Migration: cfg.VmselectMigrationURLPattern,
			Candidate: cfg.VmselectCandidateURLPattern,
		},
	}
	tenants := make([]schema.MigrationLoopTenant, 0, len(tenantIDs))
	for i, id := range tenantIDs {
		stem := stems[i]
		cmds := make([]schema.MigrationLoopCmd, 0, len(loopSteps))
		for _, b := range loopSteps {
			action, err := buildLoopAction(b.Kind, stem, cfg)
			if err != nil {
				return nil, err
			}
			cmds = append(cmds, schema.MigrationLoopCmd{
				Step:   b.Step,
				Action: action,
			})
		}
		tenants = append(tenants, schema.MigrationLoopTenant{
			Name:        id,
			TenantsFile: stem + ".json",
			StatusFile:  stem + "-status.json",
			LogFile:     stem + ".log",
			Cmds:        cmds,
		})
	}

	loopPlan.Tenants = tenants
	if err := loopPlan.ValidateTenantURLs(); err != nil {
		return nil, err
	}
	return loopPlan, nil
}

func buildLoopAction(kind schema.MigrationLoopActionKind, stem string, cfg *Config) (*schema.MigrationLoopAction, error) {
	manifest := stem + "-manifest.json"
	switch kind {
	case schema.ActionManifest:
		return &schema.MigrationLoopAction{Kind: kind, TenantsFile: stem + ".json", Manifest: manifest}, nil
	case schema.ActionMigrate:
		return &schema.MigrationLoopAction{
			Kind:           kind,
			Manifest:       manifest,
			RateLimitBytes: cfg.VmctlRateLimit,
			Concurrency:    cfg.VmctlConcurrency,
			StepInterval:   cfg.VmctlStepInterval,
		}, nil
	case schema.ActionVerify:
		return &schema.MigrationLoopAction{Kind: kind, Manifest: manifest}, nil
	default:
		return nil, fmt.Errorf("unsupported migration action kind %q", kind)
	}
}

// WriteCompiled renders + writes the compact <stem>-compile.json used for
// review and ConfigMap transport.
//
// compileOut, when non-empty, overrides the output path. Writes are non-atomic
// (jsonio.WriteFile). Like plan.json itself, this is a human-edited source
// artifact, not runtime state.
func WriteCompiled(planPath, compileOut string, cfg *Config, stages Stages) (string, error) {
	out, err := buildCompiledDoc(cfg, stages)
	if err != nil {
		return "", err
	}
	compilePath := deriveCompilePath(planPath)
	if compileOut != "" {
		compilePath = compileOut
	}
	if err := jsonio.WriteFile(compilePath, out); err != nil {
		return "", err
	}
	return compilePath, nil
}

// WriteLoopDocument serializes loopPlan as JSON to dir/migration-loop.json.
//
// The loopPlan is built once by the caller (typically via BuildMigrationLoop)
// and passed in, so WriteLoopDocument never rebuilds it. Prepare can
// therefore build once and write the runtime plan and per-tenant files from
// the same in-memory value.
//
// Non-atomic write — same rationale as WriteCompiled.
func WriteLoopDocument(dir string, loopPlan *schema.MigrationLoopPlan) (string, error) {
	loopPath := filepath.Join(dir, "migration-loop.json")
	if err := jsonio.WriteFile(loopPath, loopPlan); err != nil {
		return "", err
	}
	return loopPath, nil
}

// buildFormatCtx builds the substitution context for step desc/command
// rendering (text/template, {{.key}} syntax).
func buildFormatCtx(cfg *Config) map[string]string {
	var vmctlFlagParts []string
	if cfg.VmctlRateLimit != nil {
		vmctlFlagParts = append(vmctlFlagParts, fmt.Sprintf("--vm-rate-limit %d", *cfg.VmctlRateLimit))
	}
	if cfg.VmctlConcurrency != nil {
		vmctlFlagParts = append(vmctlFlagParts, fmt.Sprintf("--vm-concurrency %d", *cfg.VmctlConcurrency))
	}
	vmctlFlagParts = append(vmctlFlagParts, fmt.Sprintf("--vmctl-step-interval %s", cfg.VmctlStepInterval))
	vmctlFlags := strings.Join(vmctlFlagParts, " ")

	drainOrdinalsJSON, _ := json.Marshal(cfg.DrainOrdinals)
	retainOrdinalsJSON, _ := json.Marshal(cfg.RetainOrdinals)

	return map[string]string{
		"source":                      fmt.Sprintf("%d", cfg.SourceReplicaCount),
		"target":                      fmt.Sprintf("%d", cfg.TargetReplicaCount),
		"drainStart":                  fmt.Sprintf("%d", cfg.TargetReplicaCount),
		"drainEnd":                    fmt.Sprintf("%d", cfg.SourceReplicaCount-1),
		"retainStart":                 "0",
		"retainEnd":                   fmt.Sprintf("%d", cfg.TargetReplicaCount-1),
		"drainOrdinalsJson":           string(drainOrdinalsJSON),
		"retainOrdinalsJson":          string(retainOrdinalsJSON),
		"drainOrdinalsYaml":           intsCSV(cfg.DrainOrdinals),
		"retainOrdinalsYaml":          intsCSV(cfg.RetainOrdinals),
		"namespace":                   cfg.Namespace,
		"vmcluster":                   schema.VmclusterName,
		"vmselectName":                schema.VmselectName,
		"vmselectURLPattern":          cfg.VmselectURLPattern,
		"vmselectMigrationURLPattern": cfg.VmselectMigrationURLPattern,
		"vmselectCandidateURLPattern": cfg.VmselectCandidateURLPattern,
		"vminsertName":                schema.VminsertName,
		"vminsertURLPattern":          cfg.VminsertURLPattern,
		"region":                      cfg.Region,
		"windowStart":                 cfg.WindowStart,
		"windowEnd":                   cfg.WindowEnd,
		"shardStepInterval":           cfg.ShardStepInterval,
		"vmctlFlags":                  vmctlFlags,
	}
}

// renderItems applies ctx to each step's desc/command fields.
func renderItems(steps []Step, ctx map[string]string) ([]schema.CompiledStep, error) {
	out := make([]schema.CompiledStep, 0, len(steps))
	for _, s := range steps {
		rendered, err := formatStep(s, ctx)
		if err != nil {
			return nil, err
		}
		out = append(out, schema.CompiledStep{
			Name:    rendered.Name,
			Desc:    rendered.Desc,
			Command: rendered.Command,
		})
	}
	return out, nil
}

// formatStep substitutes placeholders in step.Desc and step.Command.
func formatStep(step Step, ctx map[string]string) (Step, error) {
	out := Step{Name: step.Name}
	if step.Desc != "" {
		s, err := substitute(step.Desc, ctx, step.Name)
		if err != nil {
			return out, err
		}
		out.Desc = s
	}
	if len(step.Command) > 0 {
		cmds := make([]string, 0, len(step.Command))
		for _, c := range step.Command {
			s, err := substitute(c, ctx, step.Name)
			if err != nil {
				return out, err
			}
			cmds = append(cmds, s)
		}
		out.Command = cmds
	}
	return out, nil
}

// substitute renders s as a Go text/template against ctx.
//
// Use {{.key}} syntax. missingkey=error makes unknown placeholders a hard
// error during either static compilation or tenant-specific preparation.
func substitute(s string, ctx map[string]string, stepName string) (string, error) {
	tmpl, err := template.New("step").Option("missingkey=error").Parse(s)
	if err != nil {
		return "", fmt.Errorf("plan.json step '%s' template parse: %w", stepName, err)
	}
	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, ctx); err != nil {
		return "", fmt.Errorf("plan.json step '%s': %w", stepName, err)
	}
	return buf.String(), nil
}
