// Package cluster implements the k3d cluster lifecycle state machine.
package cluster

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/tidbcloud/controlplane9/internal/config"
)

// StepStatus represents the execution status of a lifecycle step.
type StepStatus string

const (
	StatusPending     StepStatus = "pending"
	StatusRunning     StepStatus = "running"
	StatusDone        StepStatus = "done"
	StatusFailed      StepStatus = "failed"
	StatusSkipped     StepStatus = "skipped"
	StatusInterrupted StepStatus = "interrupted"
)

// StepName identifies a lifecycle step.
type StepName string

// LifecycleKind identifies the ordered lifecycle schema used by a session state.
type LifecycleKind string

const (
	LifecycleLegacyV2 LifecycleKind = "legacy-v2"
	LifecycleV3       LifecycleKind = "v3"

	StepPreflight          StepName = "preflight"
	StepConfigLoad         StepName = "config_load"
	StepOwnerIdentityReady StepName = "owner_identity_ready"
	StepIAMReady           StepName = "iam_ready"
	StepSubstrateInit      StepName = "substrate_init"
	StepSubstratePlan      StepName = "substrate_plan"
	StepSubstrateApply     StepName = "substrate_apply"
	StepHostReady          StepName = "host_ready"
	// service_plan, context_materialize and tiltfile_generate are no longer
	// stages of any current lifecycle. The names remain because destroy reads
	// them out of state files written before the Session Tiltfile cutover.
	StepServicePlan         StepName = "service_plan"
	StepContextMaterialize  StepName = "context_materialize"
	StepTiltfileGenerate    StepName = "tiltfile_generate"
	StepSessionContextWrite StepName = "session_context_write"
	StepTiltReady           StepName = "tilt_ready"
	StepSessionReady        StepName = "session_ready"
	StepStarterActivation   StepName = "starter_activation"
	StepNetworkReady        StepName = "network_ready"
	StepRemoteHostReady     StepName = "remote_host_ready"
	StepPersonaLoad         StepName = "persona_load"
	StepClusterCreate       StepName = "cluster_create"
	StepTunnelUp            StepName = "tunnel_up"
	StepCrossplaneInstall   StepName = "crossplane_install"
	StepCredWire            StepName = "cred_wire"
	StepCPCloudReady        StepName = "cp_cloud_ready"
	StepPersonaSourcesReady StepName = "persona_sources_ready"
	StepCRDInstall          StepName = "crd_install"
	StepSecretsInject       StepName = "secrets_inject"
	StepMetadbPlan          StepName = "metadb_plan"
	StepBuildReal           StepName = "build_real"
	StepGenStubs            StepName = "gen_stubs"
	StepConfigGen           StepName = "config_gen"
	StepMetadbSeed          StepName = "metadb_seed"
	StepDeploy              StepName = "deploy"
	StepReadiness           StepName = "readiness"
	StepPersonaReady        StepName = "persona_ready"

	// Deprecated aliases kept during the 07 migration to limit churn in helper
	// packages and tests. They intentionally serialize to the new lifecycle names.
	StepClassify    = StepPersonaLoad
	StepDBProvision = StepMetadbPlan
	StepAWSInject   = StepCredWire
	StepDBMigrate   = StepMetadbSeed
)

const (
	StepEssentialV2Activation StepName = "essential_v2_activation"
	StepBYOCActivation        StepName = "byoc_activation"
)

func legacySteps() []StepName {
	return []StepName{
		StepPreflight,
		StepPersonaLoad,
		StepClusterCreate,
		StepTunnelUp,
		StepCrossplaneInstall,
		StepCredWire,
		StepCPCloudReady,
		StepPersonaSourcesReady,
		StepCRDInstall,
		StepSecretsInject,
		StepMetadbPlan,
		StepBuildReal,
		StepGenStubs,
		StepConfigGen,
		StepMetadbSeed,
		StepDeploy,
		StepReadiness,
		StepPersonaReady,
	}
}

func v3Steps() []StepName {
	return []StepName{
		StepPreflight,
		StepConfigLoad,
		StepOwnerIdentityReady,
		StepIAMReady,
		StepSubstrateInit,
		StepSubstratePlan,
		StepSubstrateApply,
		StepHostReady,
		StepMetadbSeed,
		StepSessionContextWrite,
		StepTiltReady,
		StepSessionReady,
		StepStarterActivation,
		StepEssentialV2Activation,
		StepBYOCActivation,
	}
}

// AllSteps returns the legacy ordered lifecycle list kept for the cp9/v1 path.
func AllSteps() []StepName {
	return legacySteps()
}

// V3Steps returns the ordered lifecycle for v1alpha2 Premium Reserved local M1.
func V3Steps() []StepName {
	return v3Steps()
}

// StepsForLifecycle returns the ordered lifecycle sequence for the requested schema.
func StepsForLifecycle(kind LifecycleKind) []StepName {
	switch kind {
	case LifecycleV3:
		return v3Steps()
	default:
		return legacySteps()
	}
}

// StepState tracks the status and timing of a single step.
type StepState struct {
	Status    StepStatus `json:"status"`
	Error     string     `json:"error,omitempty"`
	Attempts  int        `json:"attempts,omitempty"`
	StartedAt *time.Time `json:"started_at,omitempty"`
	EndedAt   *time.Time `json:"ended_at,omitempty"`
}

// ServiceArtifact captures the deployable runtime material produced for one
// persona service during cp9 up.
type ServiceArtifact struct {
	Role          string         `json:"role,omitempty"`
	SourcePath    string         `json:"source_path,omitempty"`
	StubDir       string         `json:"stub_dir,omitempty"`
	ImageRef      string         `json:"image_ref,omitempty"`
	OverlayDir    string         `json:"overlay_dir,omitempty"`
	Namespace     string         `json:"namespace,omitempty"`
	WorkloadKind  string         `json:"workload_kind,omitempty"`
	WorkloadName  string         `json:"workload_name,omitempty"`
	ContainerName string         `json:"container_name,omitempty"`
	Ports         map[string]int `json:"ports,omitempty"`
}

// ArtifactState tracks per-service build / overlay artifacts that later
// lifecycle phases consume.
type ArtifactState struct {
	OverlaysDir string                      `json:"overlays_dir,omitempty"`
	Services    map[string]*ServiceArtifact `json:"services,omitempty"`
}

// SessionPaths records the durable artifact paths a lifecycle owns under one session.
type SessionPaths struct {
	ResolvedSession       string `json:"resolved_session,omitempty"`
	TerraformDir          string `json:"terraform_dir,omitempty"`
	TerraformSource       string `json:"terraform_source,omitempty"`
	TerraformTFVars       string `json:"terraform_tfvars,omitempty"`
	TerraformState        string `json:"terraform_state,omitempty"`
	TerraformPlan         string `json:"terraform_plan,omitempty"`
	TerraformOutputs      string `json:"terraform_outputs,omitempty"`
	TerraformContract     string `json:"terraform_contract,omitempty"`
	TerraformLogsDir      string `json:"terraform_logs_dir,omitempty"`
	GCPTerraformDir       string `json:"gcp_terraform_dir,omitempty"`
	GCPTerraformSource    string `json:"gcp_terraform_source,omitempty"`
	GCPTerraformTFVars    string `json:"gcp_terraform_tfvars,omitempty"`
	GCPTerraformState     string `json:"gcp_terraform_state,omitempty"`
	GCPTerraformPlan      string `json:"gcp_terraform_plan,omitempty"`
	GCPTerraformOutputs   string `json:"gcp_terraform_outputs,omitempty"`
	GCPTerraformLogsDir   string `json:"gcp_terraform_logs_dir,omitempty"`
	GCPRegionalHostDir    string `json:"gcp_regional_host_dir,omitempty"`
	GCPRegionalKubeconfig string `json:"gcp_regional_kubeconfig,omitempty"`
	MetadbDir             string `json:"metadb_dir,omitempty"`
	MetadbSeedReport      string `json:"metadb_seed_report,omitempty"`
	ContextDir            string `json:"context_dir,omitempty"`
	TiltDir               string `json:"tilt_dir,omitempty"`
	Tiltfile              string `json:"tiltfile,omitempty"`
	StarterRegionalSmoke  string `json:"starter_regional_smoke,omitempty"`
	DestroyDir            string `json:"destroy_dir,omitempty"`
}

// Duration returns the elapsed time for a completed step.
func (s *StepState) Duration() time.Duration {
	if s.StartedAt == nil || s.EndedAt == nil {
		return 0
	}
	return s.EndedAt.Sub(*s.StartedAt)
}

// State is the full cluster lifecycle state persisted to .cp9/<name>/state.json.
type State struct {
	ClusterName     string                  `json:"cluster_name"`
	SessionID       string                  `json:"session_id"`
	Lifecycle       LifecycleKind           `json:"lifecycle,omitempty"`
	SessionName     string                  `json:"session_name,omitempty"`
	Persona         string                  `json:"persona,omitempty"`
	HostMode        config.HostMode         `json:"host_mode,omitempty"`
	Owner           string                  `json:"owner,omitempty"`
	PlanPath        string                  `json:"plan_path,omitempty"`
	Artifacts       *ArtifactState          `json:"artifacts,omitempty"`
	Paths           *SessionPaths           `json:"paths,omitempty"`
	ReadinessChecks []ReadinessCheck        `json:"readiness_checks,omitempty"`
	CreatedAt       time.Time               `json:"created_at"`
	UpdatedAt       time.Time               `json:"updated_at"`
	Steps           map[StepName]*StepState `json:"steps"`
}

// ReadinessCheck is a named product-readiness observation emitted with the up
// summary and persisted for status/resume diagnostics.
type ReadinessCheck struct {
	Name    string `json:"name"`
	Status  string `json:"status"`
	Message string `json:"message,omitempty"`
}

// NewState creates a fresh state with all steps pending.
func NewState(clusterName, sessionID string) *State {
	return NewStateForLifecycle(clusterName, sessionID, LifecycleLegacyV2)
}

// NewStateForLifecycle creates a fresh state for the requested lifecycle schema.
func NewStateForLifecycle(clusterName, sessionID string, lifecycle LifecycleKind) *State {
	now := time.Now()
	steps := make(map[StepName]*StepState)
	for _, name := range StepsForLifecycle(lifecycle) {
		steps[name] = &StepState{Status: StatusPending}
	}
	return &State{
		ClusterName: clusterName,
		SessionID:   sessionID,
		Lifecycle:   lifecycle,
		SessionName: clusterName,
		CreatedAt:   now,
		UpdatedAt:   now,
		Steps:       steps,
	}
}

// MarkRunning marks a step as running.
func (s *State) MarkRunning(name StepName) {
	now := time.Now()
	s.Steps[name].Status = StatusRunning
	s.Steps[name].Error = ""
	s.Steps[name].Attempts++
	s.Steps[name].StartedAt = &now
	s.Steps[name].EndedAt = nil
	s.UpdatedAt = now
}

// MarkDone marks a step as completed.
func (s *State) MarkDone(name StepName) {
	now := time.Now()
	s.Steps[name].Status = StatusDone
	s.Steps[name].Error = ""
	s.Steps[name].EndedAt = &now
	s.UpdatedAt = now
}

// MarkFailed marks a step as failed with an error message.
func (s *State) MarkFailed(name StepName, err error) {
	now := time.Now()
	s.Steps[name].Status = StatusFailed
	s.Steps[name].Error = err.Error()
	s.Steps[name].EndedAt = &now
	s.UpdatedAt = now
}

// MarkInterrupted marks all running steps as interrupted.
func (s *State) MarkInterrupted() {
	now := time.Now()
	for _, step := range s.Steps {
		if step.Status == StatusRunning {
			step.Status = StatusInterrupted
			step.EndedAt = &now
		}
	}
	s.UpdatedAt = now
}

// ResumePoint returns the first step that is not done or skipped,
// indicating where to resume after a failure or interruption.
// Returns empty string if all steps are done.
func (s *State) ResumePoint() StepName {
	for _, name := range s.stepOrder() {
		st := s.Steps[name]
		if st == nil || (st.Status != StatusDone && st.Status != StatusSkipped) {
			return name
		}
	}
	return ""
}

// LifecycleState returns the active state for status display.
func (s *State) LifecycleState() StepName {
	for _, name := range s.stepOrder() {
		if st := s.Steps[name]; st != nil && st.Status == StatusRunning {
			return name
		}
	}
	return s.ResumePoint()
}

// IsComplete returns true if all steps are done or skipped.
func (s *State) IsComplete() bool {
	return s.ResumePoint() == ""
}

func (s *State) stepOrder() []StepName {
	if s == nil {
		return StepsForLifecycle(LifecycleLegacyV2)
	}
	return StepsForLifecycle(s.Lifecycle)
}

// DefaultStateDir is the root directory for per-session state relative to cwd.
const DefaultStateDir = ".cp9"

// InternalStateDirName is the deliberately invalid session-name namespace for
// CP9 metadata that must survive removal of any one session root.
const InternalStateDirName = ".internal"

// DefaultStateFile is the state file name.
const DefaultStateFile = "state.json"

// DefaultLockFile is the advisory session lock filename.
const DefaultLockFile = "lock"

// DefaultPlanFile is the persisted service-plan filename.
const DefaultPlanFile = "service-plan.json"

// DefaultArtifactsFile is the persisted lifecycle artifact file name.
const DefaultArtifactsFile = "artifacts.json"

// DefaultTerraformDir is the Terraform working directory under one session root.
const DefaultTerraformDir = "terraform"

// DefaultContextDir is the generated session context directory.
const DefaultContextDir = "context"

// DefaultMetadbDir is the generated metadb artifact directory.
const DefaultMetadbDir = "metadb"

// DefaultMetadbSeedReportFile is the persisted metadb seed report filename.
const DefaultMetadbSeedReportFile = "seed-report.json"

// DefaultO11yDir is the generated o11y artifact directory.
const DefaultO11yDir = "o11y"

// DefaultO11yStubFile is the persisted o11y stub contract filename.
const DefaultO11yStubFile = "o11y-stub.json"

// DefaultTiltDir is the generated Tilt handoff directory.
const DefaultTiltDir = "tilt"

// DefaultTiltfile is the generated Tiltfile name.
const DefaultTiltfile = "Tiltfile"

// DefaultDestroyDir is the destroy/reaper working directory.
const DefaultDestroyDir = "destroy"

// SessionStateDir returns the per-session state directory under the cp9 state root.
func SessionStateDir(sessionName string) string {
	return filepath.Join(DefaultStateDir, sessionName)
}

// StatePath returns the full path to the state file.
func StatePath(stateDir string) string {
	return filepath.Join(stateDir, DefaultStateFile)
}

// LockPath returns the full path to the session advisory lock file.
func LockPath(stateDir string) string {
	return filepath.Join(stateDir, DefaultLockFile)
}

// PlanPath returns the full path to the persisted service-plan file.
func PlanPath(stateDir string) string {
	return filepath.Join(stateDir, DefaultPlanFile)
}

// ArtifactsPath returns the full path to the persisted lifecycle artifact file.
func ArtifactsPath(stateDir string) string {
	return filepath.Join(stateDir, DefaultArtifactsFile)
}

// TerraformDir returns the session Terraform working directory.
func TerraformDir(stateDir string) string {
	return filepath.Join(stateDir, DefaultTerraformDir)
}

// ContextDir returns the generated session context directory.
func ContextDir(stateDir string) string {
	return filepath.Join(stateDir, DefaultContextDir)
}

// MetadbDir returns the metadb artifact directory.
func MetadbDir(stateDir string) string {
	return filepath.Join(stateDir, DefaultMetadbDir)
}

// MetadbSeedReportPath returns the persisted metadb seed report path.
func MetadbSeedReportPath(stateDir string) string {
	return filepath.Join(MetadbDir(stateDir), DefaultMetadbSeedReportFile)
}

// O11yDir returns the o11y artifact directory.
func O11yDir(stateDir string) string {
	return filepath.Join(stateDir, DefaultO11yDir)
}

// O11yStubPath returns the persisted o11y stub contract path.
func O11yStubPath(stateDir string) string {
	return filepath.Join(O11yDir(stateDir), DefaultO11yStubFile)
}

// TiltDir returns the generated Tilt handoff directory.
func TiltDir(stateDir string) string {
	return filepath.Join(stateDir, DefaultTiltDir)
}

// TiltfilePath returns the generated Tiltfile path.
func TiltfilePath(stateDir string) string {
	return filepath.Join(TiltDir(stateDir), DefaultTiltfile)
}

// DestroyDir returns the destroy/reaper working directory.
func DestroyDir(stateDir string) string {
	return filepath.Join(stateDir, DefaultDestroyDir)
}

// SaveState writes state to disk atomically (write unique temp + rename).
func SaveState(stateDir string, state *State) error {
	if err := os.MkdirAll(stateDir, 0o755); err != nil {
		return fmt.Errorf("create state dir: %w", err)
	}

	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal state: %w", err)
	}

	tmpFile, err := os.CreateTemp(stateDir, "state.json.*.tmp")
	if err != nil {
		return fmt.Errorf("create state temp file: %w", err)
	}
	tmpPath := tmpFile.Name()
	defer os.Remove(tmpPath)

	if _, err := tmpFile.Write(data); err != nil {
		_ = tmpFile.Close()
		return fmt.Errorf("write state temp file: %w", err)
	}
	if err := tmpFile.Close(); err != nil {
		return fmt.Errorf("close state temp file: %w", err)
	}

	target := StatePath(stateDir)
	if err := os.Rename(tmpPath, target); err != nil {
		return fmt.Errorf("rename state file: %w", err)
	}

	return nil
}

// LoadState reads state from disk.
func LoadState(stateDir string) (*State, error) {
	data, err := os.ReadFile(StatePath(stateDir))
	if err != nil {
		return nil, fmt.Errorf("read state file: %w", err)
	}

	var state State
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, fmt.Errorf("parse state file: %w", err)
	}

	return &state, nil
}

// StateExists checks if a state file exists.
// Returns true if the file exists, false if it doesn't.
// Returns an error for unexpected failures (e.g. permission denied).
func StateExists(stateDir string) (bool, error) {
	_, err := os.Stat(StatePath(stateDir))
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, fmt.Errorf("stat state file: %w", err)
}
