package lifecycle

import (
	"context"
	"fmt"
	"maps"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"

	"github.com/tidbcloud/controlplane9/internal/artifactsync"
	cpaws "github.com/tidbcloud/controlplane9/internal/aws"
	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	"github.com/tidbcloud/controlplane9/internal/host"
	"github.com/tidbcloud/controlplane9/internal/persona"
	cpterraform "github.com/tidbcloud/controlplane9/internal/terraform"
	"github.com/tidbcloud/controlplane9/internal/tunnel"
)

// ConflictChecker verifies whether a selected host collides with an orphan session.
type ConflictChecker interface {
	CheckConflict(ctx context.Context, selected *host.Session) error
}

// ConfigLoadStep writes the normalized v1alpha2 session artifact and seeds the
// durable v3 path contract under one session state directory.
type ConfigLoadStep struct {
	StateDir string
	Session  *config.SessionConfig
}

func (s *ConfigLoadStep) Name() cluster.StepName { return cluster.StepConfigLoad }
func (s *ConfigLoadStep) Run(_ context.Context) error {
	return fmt.Errorf("config_load requires lifecycle state")
}

func (s *ConfigLoadStep) RunWithState(ctx context.Context, state *cluster.State) error {
	if s.Session == nil {
		return fmt.Errorf("config_load: session config is required")
	}
	if environment := strings.TrimSpace(s.Session.Artifacts.Environment); environment != "" {
		tiltDir := filepath.Dir(s.Session.Tilt.File)
		if _, err := artifactsync.Ensure(ctx, &artifactsync.Options{
			Environment:       environment,
			OutputDir:         artifactsync.OutputDir(s.StateDir),
			GitHubAppClientID: s.Session.Services.GitHubAppClientID,
			TokenHelper:       filepath.Clean(filepath.Join(tiltDir, "..", "build", "remote", "github-app-user-token.sh")),
		}); err != nil {
			return fmt.Errorf("config_load: resolve owner artifacts: %w", err)
		}
	}
	if err := config.WriteResolvedSession(s.StateDir, s.Session); err != nil {
		return fmt.Errorf("config_load: %w", err)
	}
	state.SessionName = s.Session.Name
	state.ClusterName = s.Session.Name
	state.Persona = config.SessionPersona(s.Session)
	state.HostMode = s.Session.Host.Mode
	tfPaths := cpterraform.WorkspacePathsForVarFile(s.StateDir, s.Session.Terraform.VarFile)
	tiltfilePath := cluster.TiltfilePath(s.StateDir)
	if configured := strings.TrimSpace(s.Session.Tilt.File); configured != "" {
		tiltfilePath = configured
	}
	state.Paths = &cluster.SessionPaths{
		ResolvedSession:      config.ResolvedSessionPath(s.StateDir),
		TerraformDir:         tfPaths.RootDir,
		TerraformSource:      tfPaths.SourceDir,
		TerraformTFVars:      tfPaths.TFVarsPath,
		TerraformState:       tfPaths.StatePath,
		TerraformPlan:        tfPaths.PlanPath,
		TerraformOutputs:     tfPaths.OutputsPath,
		TerraformContract:    tfPaths.ContractPath,
		TerraformLogsDir:     tfPaths.LogsDir,
		MetadbDir:            cluster.MetadbDir(s.StateDir),
		MetadbSeedReport:     cluster.MetadbSeedReportPath(s.StateDir),
		ContextDir:           cluster.ContextDir(s.StateDir),
		TiltDir:              cluster.TiltDir(s.StateDir),
		Tiltfile:             tiltfilePath,
		StarterRegionalSmoke: StarterRegionalSmokeEvidencePath(s.StateDir),
		DestroyDir:           cluster.DestroyDir(s.StateDir),
	}
	dirs := []string{
		state.Paths.TerraformDir,
		state.Paths.TerraformLogsDir,
		state.Paths.MetadbDir,
		state.Paths.ContextDir,
		state.Paths.TiltDir,
		state.Paths.DestroyDir,
	}
	if config.DedicatedGCPEnabled(s.Session) {
		gcpPaths := cpterraform.GCPWorkspacePaths(s.StateDir)
		gcpHost := host.ResolveGCPRegionalSession(s.Session, s.StateDir)
		state.Paths.GCPTerraformDir = gcpPaths.RootDir
		state.Paths.GCPTerraformSource = gcpPaths.SourceDir
		state.Paths.GCPTerraformTFVars = gcpPaths.TFVarsPath
		state.Paths.GCPTerraformState = gcpPaths.StatePath
		state.Paths.GCPTerraformPlan = gcpPaths.PlanPath
		state.Paths.GCPTerraformOutputs = gcpPaths.OutputsPath
		state.Paths.GCPTerraformLogsDir = gcpPaths.LogsDir
		state.Paths.GCPRegionalHostDir = gcpHost.StateDir
		state.Paths.GCPRegionalKubeconfig = gcpHost.KubeconfigPath
		dirs = append(dirs, gcpPaths.RootDir, gcpPaths.LogsDir, gcpHost.StateDir)
	}
	for _, dir := range dirs {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("config_load: create %s: %w", dir, err)
		}
	}
	return nil
}

// NamedStep wraps an existing step under a different lifecycle name.
type NamedStep struct {
	StepName cluster.StepName
	Inner    cluster.Step
}

func (s *NamedStep) Name() cluster.StepName { return s.StepName }
func (s *NamedStep) Run(ctx context.Context) error {
	if s.Inner == nil {
		return nil
	}
	return s.Inner.Run(ctx)
}

func (s *NamedStep) RunWithState(ctx context.Context, state *cluster.State) error {
	if s.Inner == nil {
		return nil
	}
	if stateAware, ok := s.Inner.(cluster.StateAwareStep); ok {
		return stateAware.RunWithState(ctx, state)
	}
	return s.Inner.Run(ctx)
}

// RequiredGateStep fails closed for a lifecycle state whose contract exists
// but whose concrete behavior has not landed yet.
type RequiredGateStep struct {
	Step   cluster.StepName
	Reason string
}

func (s RequiredGateStep) Name() cluster.StepName { return s.Step }

func (s RequiredGateStep) Run(_ context.Context) error {
	msg := strings.TrimSpace(s.Reason)
	if msg == "" {
		msg = "not implemented yet"
	}
	return fmt.Errorf("%s: %s", s.Step, msg)
}

// PreflightStep captures the session metadata and validates the host/tooling surface.
type PreflightStep struct {
	Selected         *host.Session
	PersonaName      string
	TunnelMechanism  string
	ConflictChecker  ConflictChecker
	CommandLookPath  func(file string) (string, error)
	RequiredBinaries []string
}

func (s *PreflightStep) Name() cluster.StepName { return cluster.StepPreflight }
func (s *PreflightStep) Run(_ context.Context) error {
	return fmt.Errorf("preflight requires lifecycle state")
}

func (s *PreflightStep) RunWithState(ctx context.Context, state *cluster.State) error {
	if s.Selected == nil {
		return fmt.Errorf("selected host is required")
	}
	state.SessionName = s.Selected.ClusterName
	state.ClusterName = s.Selected.ClusterName
	state.Persona = s.PersonaName
	state.HostMode = s.Selected.Mode

	for _, binary := range s.requiredBinaries() {
		if _, err := s.lookPath(binary); err != nil {
			return fmt.Errorf("preflight: required binary %q not found: %w", binary, err)
		}
	}

	if preflightRequiresAppleSilicon(s.Selected.Mode, runtime.GOOS, runtime.GOARCH) {
		return fmt.Errorf("preflight: cp9 supports Apple Silicon Macs for the local-k3d path")
	}

	if s.hostProvisionPending(state) && s.ConflictChecker != nil {
		if err := s.ConflictChecker.CheckConflict(ctx, s.Selected); err != nil {
			return fmt.Errorf("preflight: %w", err)
		}
	}
	return nil
}

func preflightRequiresAppleSilicon(mode config.HostMode, goos, goarch string) bool {
	return mode == config.HostModeLocalK3d && goos == "darwin" && goarch != "arm64"
}

func (s *PreflightStep) requiredBinaries() []string {
	binaries := append([]string{"kubectl"}, s.RequiredBinaries...)
	if s.Selected.Mode == config.HostModeLocalK3d {
		binaries = append(binaries, "docker", "k3d")
	}
	if strings.TrimSpace(s.TunnelMechanism) == "tailscale" {
		binaries = append(binaries, "tailscale")
	}
	return binaries
}

func (s *PreflightStep) lookPath(file string) (string, error) {
	if s.CommandLookPath != nil {
		return s.CommandLookPath(file)
	}
	return exec.LookPath(file)
}

func (s *PreflightStep) hostProvisionPending(state *cluster.State) bool {
	for _, name := range []cluster.StepName{cluster.StepHostReady, cluster.StepClusterCreate} {
		if stepState, ok := state.Steps[name]; ok {
			return stepState.Status == cluster.StatusPending
		}
	}
	return false
}

// PersonaLoadStep materializes the per-run service plan for the legacy
// topology path. v1alpha2 sessions do not use it at all: the Session Tiltfile
// owns the service graph, and their lifecycle has no service_plan stage.
type PersonaLoadStep struct {
	StateDir      string
	PersonaName   string
	TopologiesDir string
	Local         *config.LocalConfig
}

func (s *PersonaLoadStep) Name() cluster.StepName { return cluster.StepPersonaLoad }
func (s *PersonaLoadStep) Run(_ context.Context) error {
	return fmt.Errorf("persona_load requires lifecycle state")
}

func (s *PersonaLoadStep) RunWithState(_ context.Context, state *cluster.State) error {
	topologiesDir := s.TopologiesDir
	if topologiesDir == "" {
		topologiesDir = persona.DefaultTopologiesDir
	}
	p, err := persona.Load(topologiesDir, s.PersonaName)
	if err != nil {
		return err
	}
	plan, err := persona.BuildPlan(p, localOverridesFromConfig(s.Local))
	if err != nil {
		return err
	}
	planPath := cluster.PlanPath(s.StateDir)
	if err := persona.SavePlan(planPath, plan); err != nil {
		return fmt.Errorf("persona_load: save plan: %w", err)
	}
	state.Persona = plan.Persona
	state.PlanPath = planPath
	return nil
}

// PersonaSourcesReadyStep ensures the source-service checkouts referenced by
// the plan exist locally before later build/deploy steps run.
type PersonaSourcesReadyStep struct {
	StateDir string
}

func (s *PersonaSourcesReadyStep) Name() cluster.StepName { return cluster.StepPersonaSourcesReady }

func (s *PersonaSourcesReadyStep) Run(_ context.Context) error {
	plan, err := persona.LoadPlan(cluster.PlanPath(s.StateDir))
	if err != nil {
		return fmt.Errorf("persona_sources_ready: load plan: %w", err)
	}
	for _, svc := range plan.Services {
		if svc.Role != persona.RoleSource || svc.Source == nil {
			continue
		}
		if _, err := os.Stat(svc.Source.Path); err != nil {
			return fmt.Errorf("persona_sources_ready: %s source %q: %w", svc.Name, svc.Source.Path, err)
		}
	}
	return nil
}

// CPCloudReadyStep gates downstream states on the initial C1/C2 artifacts.
type CPCloudReadyStep struct {
	StateDir string
}

func (s *CPCloudReadyStep) Name() cluster.StepName { return cluster.StepCPCloudReady }

func (s *CPCloudReadyStep) Run(_ context.Context) error {
	tunnelState, err := tunnel.LoadState(s.StateDir)
	if err != nil {
		return fmt.Errorf("cp_cloud_ready: load tunnel state: %w", err)
	}
	if !tunnelState.Healthy {
		return fmt.Errorf("cp_cloud_ready: tunnel is not healthy")
	}
	if !cpaws.SessionStateExists(s.StateDir) {
		return fmt.Errorf("cp_cloud_ready: AWS credential state is missing")
	}
	return nil
}

func localOverridesFromConfig(local *config.LocalConfig) *persona.LocalOverrides {
	if local == nil {
		return &persona.LocalOverrides{}
	}
	sources := make(map[string]persona.SourceRef, len(local.Sources))
	for svc, src := range local.Sources {
		sources[svc] = persona.SourceRef{Path: src.Path, Ref: src.Ref}
	}
	return &persona.LocalOverrides{
		Sources:   sources,
		ForceReal: append([]string(nil), local.Overrides.ForceReal...),
		ForceStub: append([]string(nil), local.Overrides.ForceStub...),
		ExtraEnv:  cloneExtraEnv(local.Overrides.ExtraEnv),
	}
}

func cloneExtraEnv(src map[string]map[string]string) map[string]map[string]string {
	if len(src) == 0 {
		return nil
	}
	out := make(map[string]map[string]string, len(src))
	for svc, env := range src {
		cp := make(map[string]string, len(env))
		maps.Copy(cp, env)
		out[svc] = cp
	}
	return out
}
