package lifecycle

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"time"

	cpaws "github.com/tidbcloud/controlplane9/internal/aws"
	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	"github.com/tidbcloud/controlplane9/internal/db"
	"github.com/tidbcloud/controlplane9/internal/host"
	localk3d "github.com/tidbcloud/controlplane9/internal/host/local/k3d"
	remoteec2 "github.com/tidbcloud/controlplane9/internal/host/remote/ec2k3d"
	"github.com/tidbcloud/controlplane9/internal/kustomize"
	"github.com/tidbcloud/controlplane9/internal/migrate"
	"github.com/tidbcloud/controlplane9/internal/persona"
	"github.com/tidbcloud/controlplane9/internal/stub"
	"github.com/tidbcloud/controlplane9/internal/verbose"
)

var (
	namespaceEnsureMaxWait = 2 * time.Minute
	retryAttemptInterval   = 500 * time.Millisecond
)

type imageImporter interface {
	ImportImage(ctx context.Context, selected *host.Session, image string) error
}

type metadbProvisionFunc func(ctx context.Context, cfg *config.Config, services []string, stateDir string) (*db.ProvisionResult, error)

// MetadbPlanStep provisions the active metadb provider once per session.
type MetadbPlanStep struct {
	StateDir  string
	Config    *config.Config
	Provision metadbProvisionFunc
}

func (s *MetadbPlanStep) Name() cluster.StepName { return cluster.StepMetadbPlan }

func (s *MetadbPlanStep) Run(ctx context.Context) error {
	if s.Config == nil {
		return fmt.Errorf("metadb_plan: config is required")
	}
	if _, err := db.AdminDSN(s.StateDir); err == nil {
		return nil
	}

	plan, err := persona.LoadPlan(cluster.PlanPath(s.StateDir))
	if err != nil {
		return fmt.Errorf("metadb_plan: load plan: %w", err)
	}

	services := planServiceNames(plan, func(svc persona.ServicePlan) bool {
		return svc.Role != persona.RoleSkip
	})
	if len(services) == 0 {
		return nil
	}

	provision := s.Provision
	if provision == nil {
		provision = db.Provision
	}

	if _, err := provision(ctx, s.Config, services, s.StateDir); err != nil {
		return fmt.Errorf("metadb_plan: provision: %w", err)
	}
	return nil
}

// BuildRealStep compiles source services from local source, packages session
// images, and imports them into the selected host.
type BuildRealStep struct {
	StateDir      string
	Selected      *host.Session
	Services      []string
	ForceRebuild  bool
	Importer      imageImporter
	CommandRunner func(cmd *exec.Cmd) ([]byte, error)
}

func (s *BuildRealStep) Name() cluster.StepName { return cluster.StepBuildReal }

func (s *BuildRealStep) Run(ctx context.Context) error {
	if s.Selected == nil {
		return fmt.Errorf("build_real: selected host is required")
	}

	plan, err := persona.LoadPlan(cluster.PlanPath(s.StateDir))
	if err != nil {
		return fmt.Errorf("build_real: load plan: %w", err)
	}
	artifacts := loadOrInitArtifacts(s.StateDir)

	importer := s.Importer
	if importer == nil {
		importer = defaultImageImporter(s.Selected)
	}

	services := sortedServicePlans(plan, func(svc persona.ServicePlan) bool {
		return svc.Role == persona.RoleSource
	})
	for _, svc := range services {
		if !serviceSelected(s.Services, svc.Name) {
			continue
		}
		if svc.Source == nil {
			return fmt.Errorf("build_real: %s has no source override", svc.Name)
		}
		if !s.ForceRebuild {
			if existing := artifacts.Services[svc.Name]; existing != nil && existing.ImageRef != "" && existing.SourcePath == svc.Source.Path {
				continue
			}
		}
		if err := validateSourcePath(svc.Source.Path); err != nil {
			return fmt.Errorf("build_real: %s source %q: %w", svc.Name, svc.Source.Path, err)
		}

		imageRef := serviceImageRef(s.Selected.ClusterName, svc.Name)
		if err := s.buildGoBinaryImage(ctx, svc.Name, svc.Source.Path, imageRef); err != nil {
			return fmt.Errorf("build_real: %s: %w", svc.Name, err)
		}
		if err := importer.ImportImage(ctx, s.Selected, imageRef); err != nil {
			return fmt.Errorf("build_real: import %s: %w", svc.Name, err)
		}

		artifacts.Services[svc.Name] = &cluster.ServiceArtifact{
			Role:       string(persona.RoleSource),
			SourcePath: svc.Source.Path,
			ImageRef:   imageRef,
		}
		if err := SaveArtifacts(s.StateDir, artifacts); err != nil {
			return fmt.Errorf("build_real: persist artifacts: %w", err)
		}
	}

	return nil
}

func (s *BuildRealStep) buildGoBinaryImage(ctx context.Context, serviceName, sourcePath, imageRef string) error {
	workDir := filepath.Join(s.StateDir, "images", sanitizeName(filepath.Base(filepath.Dir(sourcePath))))
	if err := os.MkdirAll(workDir, 0o755); err != nil {
		return fmt.Errorf("create work dir: %w", err)
	}
	absWorkDir, err := filepath.Abs(workDir)
	if err != nil {
		return fmt.Errorf("resolve work dir: %w", err)
	}
	binaryPath := filepath.Join(absWorkDir, "server")
	targetArch := containerArch(s.Selected)

	buildCmd := exec.CommandContext(ctx, "go", "build", "-o", binaryPath, ".")
	buildCmd.Dir = filepath.Dir(sourcePath)
	buildCmd.Env = append(buildCmd.Environ(),
		"CGO_ENABLED=0",
		"GOOS=linux",
		"GOARCH="+targetArch,
	)
	if output, err := s.runCommand(buildCmd); err != nil {
		return fmt.Errorf("go build: %w\n%s", err, string(output))
	}

	dockerfile := renderBuildRealDockerfile(serviceName)
	if err := os.WriteFile(filepath.Join(absWorkDir, "Dockerfile"), []byte(dockerfile), 0o600); err != nil {
		return fmt.Errorf("write Dockerfile: %w", err)
	}

	//nolint:gosec // imageRef/workDir are derived from validated session/service names and local temp dirs.
	dockerCmd := exec.CommandContext(ctx, "docker", "build", "--platform=linux/"+targetArch, "-t", imageRef, absWorkDir)
	if output, err := s.runCommand(dockerCmd); err != nil {
		return fmt.Errorf("docker build: %w\n%s", err, string(output))
	}
	return nil
}

func renderBuildRealDockerfile(serviceName string) string {
	var b strings.Builder
	b.WriteString("FROM public.ecr.aws/docker/library/alpine:3.19\n")
	if sourceServiceNeedsAWSIAMAuthenticatorRuntimeToolchain(serviceName) {
		appendAWSIAMAuthenticatorRuntimeToolchain(&b)
	} else {
		b.WriteString("RUN apk add --no-cache ca-certificates\n")
	}
	b.WriteString(`COPY server /usr/local/bin/service
ENTRYPOINT ["/usr/local/bin/service"]
`)
	return b.String()
}

func (s *BuildRealStep) runCommand(cmd *exec.Cmd) ([]byte, error) {
	if s.CommandRunner != nil {
		return s.CommandRunner(cmd)
	}
	return verbose.RunCommand(cmd)
}

// GenStubsStep generates permissive stubs, builds local images, and imports
// them into the selected host.
type GenStubsStep struct {
	StateDir      string
	Selected      *host.Session
	Services      []string
	Force         bool
	Importer      imageImporter
	CommandRunner func(cmd *exec.Cmd) ([]byte, error)
}

func (s *GenStubsStep) Name() cluster.StepName { return cluster.StepGenStubs }

func (s *GenStubsStep) Run(ctx context.Context) error {
	if s.Selected == nil {
		return fmt.Errorf("gen_stubs: selected host is required")
	}

	plan, err := persona.LoadPlan(cluster.PlanPath(s.StateDir))
	if err != nil {
		return fmt.Errorf("gen_stubs: load plan: %w", err)
	}
	artifacts := loadOrInitArtifacts(s.StateDir)

	importer := s.Importer
	if importer == nil {
		importer = defaultImageImporter(s.Selected)
	}
	targetArch := containerArch(s.Selected)

	services := sortedServicePlans(plan, func(svc persona.ServicePlan) bool {
		return svc.Role == persona.RoleStub
	})
	for _, svc := range services {
		if !serviceSelected(s.Services, svc.Name) {
			continue
		}
		imageRef := serviceImageRef(s.Selected.ClusterName, svc.Name)
		stubDir := filepath.Join(s.StateDir, "stubs", svc.Name)
		stubCfg := stubConfigForServicePlan(&svc, stubDir)
		stubCfg.Force = s.Force

		if _, err := stub.Generate(stubCfg); err != nil {
			return fmt.Errorf("gen_stubs: generate %s: %w", svc.Name, err)
		}

		//nolint:gosec // imageRef/stubDir/arch are derived from validated session/service names and local temp dirs.
		dockerCmd := exec.CommandContext(
			ctx,
			"docker",
			"build",
			"--platform=linux/"+targetArch,
			"--build-arg", "TARGETARCH="+targetArch,
			"-t", imageRef,
			stubDir,
		)
		if output, err := s.runCommand(dockerCmd); err != nil {
			return fmt.Errorf("gen_stubs: docker build %s: %w\n%s", svc.Name, err, string(output))
		}
		if err := importer.ImportImage(ctx, s.Selected, imageRef); err != nil {
			return fmt.Errorf("gen_stubs: import %s: %w", svc.Name, err)
		}

		artifacts.Services[svc.Name] = &cluster.ServiceArtifact{
			Role:     string(persona.RoleStub),
			StubDir:  stubDir,
			ImageRef: imageRef,
			Ports: map[string]int{
				"http": stub.DefaultHTTPPort,
				"grpc": stub.DefaultGRPCPort,
			},
		}
		if err := SaveArtifacts(s.StateDir, artifacts); err != nil {
			return fmt.Errorf("gen_stubs: persist artifacts: %w", err)
		}
	}

	return nil
}

func stubConfigForServicePlan(svc *persona.ServicePlan, stubDir string) *stub.Config {
	cfg := &stub.Config{
		Service:   svc.Name,
		Kind:      stub.RecordingKindForService(svc.Name),
		OutputDir: stubDir,
		HTTPPort:  stub.DefaultHTTPPort,
		GRPCPort:  stub.DefaultGRPCPort,
	}
	if svc.Stub == nil {
		return cfg
	}
	if kind := strings.TrimSpace(svc.Stub.Kind); kind != "" {
		cfg.Kind = stub.Kind(kind)
	}
	if protoPath := strings.TrimSpace(svc.Stub.Proto); protoPath != "" {
		cfg.ProtoFiles = []string{protoPath}
	}
	cfg.SwaggerFile = strings.TrimSpace(svc.Stub.Swagger)
	return cfg
}

func (s *GenStubsStep) runCommand(cmd *exec.Cmd) ([]byte, error) {
	if s.CommandRunner != nil {
		return s.CommandRunner(cmd)
	}
	return verbose.RunCommand(cmd)
}

func defaultImageImporter(selected *host.Session) imageImporter {
	if selected != nil && selected.Mode == config.HostModeRemoteEC2K3d {
		return remoteec2.NewAdapter()
	}
	return localk3d.NewAdapter()
}

// ConfigGenStep turns the persona plan plus built image refs into deployable overlays.
type ConfigGenStep struct {
	StateDir string
	Config   *config.Config
}

func (s *ConfigGenStep) Name() cluster.StepName { return cluster.StepConfigGen }

func (s *ConfigGenStep) Run(_ context.Context) error {
	if s.Config == nil {
		return fmt.Errorf("config_gen: config is required")
	}

	plan, err := persona.LoadPlan(cluster.PlanPath(s.StateDir))
	if err != nil {
		return fmt.Errorf("config_gen: load plan: %w", err)
	}
	artifacts := loadOrInitArtifacts(s.StateDir)

	serviceNames := planServiceNames(plan, func(svc persona.ServicePlan) bool {
		return svc.Role != persona.RoleSkip
	})
	dsns, err := db.GenerateDSNs(s.StateDir, serviceNames)
	if err != nil {
		return fmt.Errorf("config_gen: generate DSNs: %w", err)
	}

	images := make(map[string]string, len(artifacts.Services))
	for name, svc := range artifacts.Services {
		if svc != nil && svc.ImageRef != "" {
			images[name] = svc.ImageRef
		}
	}

	var stagingConfigs map[string]*kustomize.StagingConfig
	if s.Config.Staging.AppDelivery != "" {
		stagingConfigs, err = kustomize.ParseStagingConfigs(s.Config.Staging.AppDelivery)
		if err != nil {
			return fmt.Errorf("config_gen: parse staging configs: %w", err)
		}
	}

	result, err := kustomize.Generate(&kustomize.OverlayInput{
		Plan:           plan,
		Images:         images,
		DSNs:           dsns,
		AWS:            s.Config.Clouds.AWS,
		Owner:          resolveOwnerForOverlay(s.StateDir),
		Session:        s.Config.Name,
		Namespace:      s.Config.Name,
		StagingConfigs: stagingConfigs,
		StagingRoot:    s.Config.Staging.AppDelivery,
	}, filepath.Join(s.StateDir, "overlays"))
	if err != nil {
		return fmt.Errorf("config_gen: %w", err)
	}

	artifacts.OverlaysDir = result.OutputDir
	for _, name := range result.Services {
		overlay, ok := result.Overlays[name]
		if !ok {
			continue
		}
		svcArtifact := artifacts.Services[name]
		if svcArtifact == nil {
			svcArtifact = &cluster.ServiceArtifact{}
			artifacts.Services[name] = svcArtifact
		}
		svcArtifact.OverlayDir = overlay.Directory
		svcArtifact.Namespace = overlay.Namespace
		svcArtifact.WorkloadKind = overlay.WorkloadKind
		svcArtifact.WorkloadName = overlay.WorkloadName
		svcArtifact.ContainerName = overlay.ContainerName
		svcArtifact.ImageRef = overlay.Image
		svcArtifact.Ports = overlay.Ports
	}

	if err := SaveArtifacts(s.StateDir, artifacts); err != nil {
		return fmt.Errorf("config_gen: persist artifacts: %w", err)
	}
	return nil
}

// MetadbSeedStep mirrors staging schema/data into the session metadb when the
// caller provided staging DSNs for the source services.
type MetadbSeedStep struct {
	StateDir   string
	SeedConfig config.SeedConfig
}

func (s *MetadbSeedStep) Name() cluster.StepName { return cluster.StepMetadbSeed }

func (s *MetadbSeedStep) Run(ctx context.Context) error {
	plan, err := persona.LoadPlan(cluster.PlanPath(s.StateDir))
	if err != nil {
		return fmt.Errorf("metadb_seed: load plan: %w", err)
	}
	services := planServiceNames(plan, func(svc persona.ServicePlan) bool {
		return svc.Role == persona.RoleSource
	})
	if len(services) == 0 {
		return nil
	}

	localDSNs, err := db.GenerateDSNs(s.StateDir, services)
	if err != nil {
		return fmt.Errorf("metadb_seed: local DSNs: %w", err)
	}
	adminDSN, err := db.AdminDSN(s.StateDir)
	if err != nil {
		return fmt.Errorf("metadb_seed: admin DSN: %w", err)
	}

	result := migrate.Run(ctx, &migrate.Options{
		Services:    services,
		StagingDSNs: resolveStagingDSNs(services),
		LocalDSNs:   localDSNs,
		AdminDSN:    adminDSN,
		SeedConfig:  s.SeedConfig,
		StateDir:    s.StateDir,
	})
	for _, svc := range result.Services {
		if svc.Status == "failed" {
			return fmt.Errorf("metadb_seed: %s: %s", svc.Service, svc.Error)
		}
	}
	return nil
}

// DeployStep applies each generated service overlay to the selected host cluster.
type DeployStep struct {
	StateDir      string
	Selected      *host.Session
	Services      []string
	ForceRollout  bool
	CommandRunner func(cmd *exec.Cmd) ([]byte, error)
	KubectlRunner func(ctx context.Context, selected *host.Session, stdin []byte, args ...string) ([]byte, error)
}

func (s *DeployStep) Name() cluster.StepName { return cluster.StepDeploy }

func (s *DeployStep) Run(ctx context.Context) error {
	if s.Selected == nil {
		return fmt.Errorf("deploy: selected host is required")
	}
	if err := s.ensureNamespace(ctx); err != nil {
		return err
	}

	artifacts, err := LoadArtifacts(s.StateDir)
	if err != nil {
		return fmt.Errorf("deploy: load artifacts: %w", err)
	}
	serviceNames := artifactServiceNames(artifacts)
	for _, name := range serviceNames {
		if !serviceSelected(s.Services, name) {
			continue
		}
		artifact := artifacts.Services[name]
		if artifact == nil || artifact.OverlayDir == "" {
			continue
		}
		if err := s.applyOverlay(ctx, name, artifact); err != nil {
			return err
		}
		if s.ForceRollout {
			if err := s.restartWorkload(ctx, artifact); err != nil {
				return fmt.Errorf("deploy: rollout restart %s: %w", name, err)
			}
		}
	}
	return nil
}

func (s *DeployStep) applyOverlay(ctx context.Context, name string, artifact *cluster.ServiceArtifact) error {
	if s.Selected != nil && s.Selected.Mode == config.HostModeRemoteEC2K3d {
		rendered, err := s.renderOverlay(ctx, artifact.OverlayDir)
		if err != nil {
			return fmt.Errorf("deploy: render %s: %w", name, err)
		}
		if output, err := s.runKubectl(ctx, rendered, "apply", "-f", "-"); err != nil {
			return fmt.Errorf("deploy: apply %s: %w\n%s", name, err, string(output))
		}
		return nil
	}
	if output, err := s.runKubectl(ctx, nil, "apply", "-k", artifact.OverlayDir); err != nil {
		return fmt.Errorf("deploy: apply %s: %w\n%s", name, err, string(output))
	}
	return nil
}

func (s *DeployStep) renderOverlay(ctx context.Context, overlayDir string) ([]byte, error) {
	cmd := exec.CommandContext(ctx, "kubectl", "kustomize", overlayDir)
	if s.CommandRunner != nil {
		output, err := s.CommandRunner(cmd)
		if err != nil {
			return nil, fmt.Errorf("%w\n%s", err, string(output))
		}
		return output, nil
	}
	output, err := verbose.RunCommand(cmd)
	if err != nil {
		return nil, fmt.Errorf("%w\n%s", err, string(output))
	}
	return output, nil
}

func (s *DeployStep) ensureNamespace(ctx context.Context) error {
	namespace := s.Selected.Namespace
	if namespace == "" || namespace == "default" {
		return nil
	}

	deadline := time.Now().Add(namespaceEnsureMaxWait)
	for {
		// Fast path: namespace already exists.
		output, err := s.runKubectl(ctx, nil, "get", "namespace", namespace, "-o", "name")
		if err == nil && strings.TrimSpace(string(output)) != "" {
			return nil
		}
		if err != nil && isTransientNamespaceEnsureError(output, err) && time.Now().Before(deadline) {
			if retryErr := sleepUntilRetryAttempt(ctx); retryErr != nil {
				return retryErr
			}
			continue
		}

		output, err = s.runKubectl(ctx, nil, "create", "namespace", namespace)
		if err == nil || strings.Contains(string(output), "already exists") {
			return nil
		}
		if isTransientNamespaceEnsureError(output, err) && time.Now().Before(deadline) {
			if retryErr := sleepUntilRetryAttempt(ctx); retryErr != nil {
				return retryErr
			}
			continue
		}
		return fmt.Errorf("deploy: ensure namespace %s: %w\n%s", namespace, err, string(output))
	}
}

func isTransientNamespaceEnsureError(output []byte, err error) bool {
	if err == nil {
		return false
	}
	text := strings.ToLower(strings.TrimSpace(string(output)))
	if text == "" {
		text = strings.ToLower(err.Error())
	} else {
		text = text + "\n" + strings.ToLower(err.Error())
	}
	return strings.Contains(text, "unable to connect to the server") ||
		strings.Contains(text, "connection refused") ||
		strings.Contains(text, "tls handshake timeout") ||
		strings.Contains(text, "i/o timeout") ||
		strings.Contains(text, "unexpected eof") ||
		strings.Contains(text, "eof")
}

func sleepUntilRetryAttempt(ctx context.Context) error {
	timer := time.NewTimer(retryAttemptInterval)
	defer timer.Stop()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func (s *DeployStep) restartWorkload(ctx context.Context, artifact *cluster.ServiceArtifact) error {
	if artifact == nil || artifact.WorkloadKind == "" || artifact.WorkloadName == "" {
		return nil
	}

	namespace := artifact.Namespace
	if strings.TrimSpace(namespace) == "" {
		namespace = s.Selected.Namespace
	}

	args := []string{
		"-n", namespace,
		"rollout", "restart", strings.ToLower(artifact.WorkloadKind) + "/" + artifact.WorkloadName,
	}
	if output, err := s.runKubectl(ctx, nil, args...); err != nil {
		return fmt.Errorf("%w\n%s", err, string(output))
	}
	return nil
}

func (s *DeployStep) runKubectl(ctx context.Context, stdin []byte, args ...string) ([]byte, error) {
	if s.KubectlRunner != nil {
		return s.KubectlRunner(ctx, s.Selected, stdin, args...)
	}
	if s.CommandRunner != nil && s.Selected != nil && s.Selected.Mode == config.HostModeLocalK3d {
		//nolint:gosec // kubectl args are derived from validated session metadata and generated callers.
		cmd := exec.CommandContext(ctx, "kubectl", s.Selected.KubectlArgs(args...)...)
		if len(stdin) > 0 {
			cmd.Stdin = bytes.NewReader(stdin)
		}
		return s.CommandRunner(cmd)
	}
	return host.RunKubectl(ctx, s.Selected, stdin, args...)
}

// ReadinessStep waits for each rendered workload rollout to become ready.
type ReadinessStep struct {
	StateDir      string
	Selected      *host.Session
	Config        *config.Config
	Services      []string
	CommandRunner func(cmd *exec.Cmd) ([]byte, error)
	Timeout       time.Duration
}

func (s *ReadinessStep) Name() cluster.StepName { return cluster.StepReadiness }

func (s *ReadinessStep) Run(ctx context.Context) error {
	if s.Selected == nil {
		return fmt.Errorf("readiness: selected host is required")
	}
	artifacts, err := LoadArtifacts(s.StateDir)
	if err != nil {
		return fmt.Errorf("readiness: load artifacts: %w", err)
	}

	timeout := s.Timeout
	if timeout == 0 {
		timeout = 3 * time.Minute
	}

	for _, name := range artifactServiceNames(artifacts) {
		if !serviceSelected(s.Services, name) {
			continue
		}
		artifact := artifacts.Services[name]
		if artifact == nil || artifact.WorkloadKind == "" || artifact.WorkloadName == "" {
			continue
		}
		kind := strings.ToLower(artifact.WorkloadKind)
		args := []string{
			"-n", artifact.Namespace,
			"rollout", "status", kind + "/" + artifact.WorkloadName,
			"--timeout", timeout.String(),
		}
		if output, err := s.runKubectl(ctx, args...); err != nil {
			return fmt.Errorf("readiness: %s: %w\n%s", name, err, string(output))
		}
	}

	return nil
}

func (s *ReadinessStep) runKubectl(ctx context.Context, args ...string) ([]byte, error) {
	if s.CommandRunner != nil && s.Selected != nil && s.Selected.Mode == config.HostModeLocalK3d {
		//nolint:gosec // kubectl args are derived from validated session metadata and generated callers.
		cmd := exec.CommandContext(ctx, "kubectl", s.Selected.KubectlArgs(args...)...)
		return s.CommandRunner(cmd)
	}
	return host.RunKubectl(ctx, s.Selected, nil, args...)
}

// PersonaReadyStep mirrors the generated artifact metadata back into the
// persisted lifecycle state once every service is ready.
type PersonaReadyStep struct {
	StateDir string
}

func (s *PersonaReadyStep) Name() cluster.StepName { return cluster.StepPersonaReady }
func (s *PersonaReadyStep) Run(_ context.Context) error {
	return fmt.Errorf("persona_ready requires lifecycle state")
}

func (s *PersonaReadyStep) RunWithState(_ context.Context, state *cluster.State) error {
	artifacts, err := LoadArtifacts(s.StateDir)
	if err != nil {
		return fmt.Errorf("persona_ready: load artifacts: %w", err)
	}
	state.Artifacts = artifacts
	return nil
}

func loadOrInitArtifacts(stateDir string) *cluster.ArtifactState {
	artifacts, err := LoadArtifacts(stateDir)
	if err == nil {
		return artifacts
	}
	return &cluster.ArtifactState{
		Services: make(map[string]*cluster.ServiceArtifact),
	}
}

func planServiceNames(plan *persona.Plan, keep func(persona.ServicePlan) bool) []string {
	services := sortedServicePlans(plan, keep)
	names := make([]string, 0, len(services))
	for _, svc := range services {
		names = append(names, svc.Name)
	}
	return names
}

func serviceSelected(selected []string, name string) bool {
	if len(selected) == 0 {
		return true
	}
	return slices.Contains(selected, name)
}

func sortedServicePlans(plan *persona.Plan, keep func(persona.ServicePlan) bool) []persona.ServicePlan {
	if plan == nil {
		return nil
	}
	var services []persona.ServicePlan
	for _, svc := range plan.Services {
		if keep == nil || keep(svc) {
			services = append(services, svc)
		}
	}
	slices.SortFunc(services, func(a, b persona.ServicePlan) int {
		return strings.Compare(a.Name, b.Name)
	})
	return services
}

func artifactServiceNames(artifacts *cluster.ArtifactState) []string {
	if artifacts == nil {
		return nil
	}
	names := make([]string, 0, len(artifacts.Services))
	for name := range artifacts.Services {
		names = append(names, name)
	}
	slices.Sort(names)
	return names
}

func validateSourcePath(path string) error {
	if filepath.Base(path) != "main.go" {
		return fmt.Errorf("source path must point to main.go")
	}
	info, err := os.Stat(path)
	if err != nil {
		return err
	}
	if info.IsDir() {
		return fmt.Errorf("source path must be a file, got directory")
	}
	return nil
}

func serviceImageRef(sessionName, serviceName string) string {
	return fmt.Sprintf("cp9.local/%s-%s:local", sanitizeName(sessionName), sanitizeName(serviceName))
}

func sanitizeName(value string) string {
	value = strings.ToLower(value)
	value = strings.ReplaceAll(value, "_", "-")
	var b strings.Builder
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z':
			_, _ = b.WriteRune(r)
		case r >= '0' && r <= '9':
			_, _ = b.WriteRune(r)
		case r == '-':
			_, _ = b.WriteRune(r)
		default:
			_, _ = b.WriteRune('-')
		}
	}
	return strings.Trim(b.String(), "-")
}

func resolveOwnerForOverlay(stateDir string) string {
	state, err := cpaws.LoadSessionState(stateDir)
	if err != nil {
		return ""
	}
	return state.Owner
}

func resolveStagingDSNs(services []string) map[string]string {
	dsns := make(map[string]string, len(services))
	for _, svc := range services {
		if val := os.Getenv("STAGING_DSN_" + toEnvKey(svc)); val != "" {
			dsns[svc] = val
		}
	}
	return dsns
}

func toEnvKey(name string) string {
	var b strings.Builder
	for _, r := range name {
		switch {
		case r == '-':
			_ = b.WriteByte('_')
		case r >= 'a' && r <= 'z':
			_ = b.WriteByte(byte(r - 32))
		default:
			_, _ = b.WriteRune(r)
		}
	}
	return b.String()
}

func containerArch(selected *host.Session) string {
	if selected != nil && selected.Mode == config.HostModeRemoteEC2K3d {
		return "arm64"
	}
	if selected != nil && selected.Mode == config.HostModeRemoteEKS {
		return "amd64"
	}
	arch := strings.TrimSpace(os.Getenv("GOARCH"))
	if arch == "" {
		arch = runtime.GOARCH
	}
	switch arch {
	case "arm64", "amd64":
		return arch
	default:
		return "amd64"
	}
}
