package lifecycle

import (
	"context"
	"encoding/json"
	"fmt"
	"maps"
	"os"
	"path/filepath"
	"strings"
	"time"

	cpaws "github.com/tidbcloud/controlplane9/internal/aws"
	"github.com/tidbcloud/controlplane9/internal/cluster"
	"github.com/tidbcloud/controlplane9/internal/config"
	cp9errors "github.com/tidbcloud/controlplane9/internal/errors"
	cpterraform "github.com/tidbcloud/controlplane9/internal/terraform"
)

// SubstrateInitStep validates and materializes the Terraform substrate source.
type SubstrateInitStep struct {
	StateDir string
	Session  *config.SessionConfig
	Runner   cpterraform.Runner
}

func (s *SubstrateInitStep) Name() cluster.StepName { return cluster.StepSubstrateInit }

func (s *SubstrateInitStep) Run(_ context.Context) error {
	return fmt.Errorf("substrate_init requires lifecycle state")
}

func (s *SubstrateInitStep) RunWithState(ctx context.Context, state *cluster.State) error {
	if s.Session == nil {
		return cp9errors.New(cp9errors.CategoryConfig, "substrate_init: session config is required")
	}
	varFilePath := strings.TrimSpace(s.Session.Terraform.VarFile)
	if varFilePath == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "substrate_init: terraform.var_file is required")
	}
	paths := cpterraform.WorkspacePathsForVarFile(s.StateDir, varFilePath)
	if err := s.Runner.Init(ctx, &paths); err != nil {
		return fmt.Errorf("substrate_init: %w", err)
	}
	if err := s.Runner.VerifyGitRef(ctx, s.Session.Terraform.Repo, s.Session.Terraform.Ref); err != nil {
		return fmt.Errorf("substrate_init: %w", err)
	}
	if _, err := os.Stat(varFilePath); err != nil {
		return fmt.Errorf("substrate_init: %w", cp9errors.Newf(cp9errors.CategoryConfig,
			"terraform.var_file %q not found: %v", s.Session.Terraform.VarFile, err))
	}
	if err := cpterraform.MaterializeSource(s.Session.Terraform.Repo, s.Session.Terraform.Path, paths.SourceDir); err != nil {
		return fmt.Errorf("substrate_init: %w", err)
	}
	return nil
}

// SubstratePlanStep writes tfvars, runs terraform init/plan, and applies the no-EKS guard.
type SubstratePlanStep struct {
	StateDir string
	Session  *config.SessionConfig
	Runner   cpterraform.Runner
}

func (s *SubstratePlanStep) Name() cluster.StepName { return cluster.StepSubstratePlan }

func (s *SubstratePlanStep) Run(_ context.Context) error {
	return fmt.Errorf("substrate_plan requires lifecycle state")
}

func (s *SubstratePlanStep) RunWithState(ctx context.Context, state *cluster.State) error {
	if s.Session == nil {
		return cp9errors.New(cp9errors.CategoryConfig, "substrate_plan: session config is required")
	}
	if strings.TrimSpace(state.Owner) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "substrate_plan: lifecycle owner is required")
	}
	awsState, err := cpaws.LoadSessionState(s.StateDir)
	if err != nil {
		return fmt.Errorf("substrate_plan: load aws state: %w", err)
	}
	paths := cpterraform.WorkspacePathsForVarFile(s.StateDir, s.Session.Terraform.VarFile)
	tags, err := substrateTags(s.Session, awsState)
	if err != nil {
		return fmt.Errorf("substrate_plan: %w", err)
	}
	terraformOwner := strings.TrimSpace(awsState.SessionTags[cpaws.TagOwner])
	if terraformOwner == "" {
		terraformOwner = state.Owner
	}
	inputs := cpterraform.SessionInputs{
		SessionName:                s.Session.Name,
		Owner:                      terraformOwner,
		TargetCloud:                s.Session.Target.Cloud,
		TargetRegion:               s.Session.Target.Region,
		TargetAccount:              awsState.Account,
		CP9DNSZoneID:               s.Session.Host.Exposure.ZoneID,
		CP9DNSZoneName:             s.Session.Host.Exposure.ZoneName,
		AWSProfile:                 awsState.Profile,
		AWSSourceProfile:           awsState.SourceProfile,
		Tags:                       tags,
		RemoteEC2K3DHost:           s.Session.Host.Mode == config.HostModeRemoteEC2K3d,
		RemoteEKSEnabled:           s.Session.Target.AWS.RemoteEKS.Enabled,
		RemoteEKSClusterName:       s.Session.Target.AWS.RemoteEKS.ClusterName,
		RemoteEKSKubernetesVersion: s.Session.Target.AWS.RemoteEKS.KubernetesVersion,
		RemoteEKSNodeType:          s.Session.Target.AWS.RemoteEKS.NodeInstanceType,
		RemoteEKSNodeCount:         s.Session.Target.AWS.RemoteEKS.NodeCount,
		RemoteEKSNodeGroups:        cpterraform.RemoteEKSNodeGroupInputs(s.Session.Target.AWS.RemoteEKS.NodeGroups),
	}
	if err := cpterraform.MaterializeTFVars(s.Session.Terraform.VarFile, paths.TFVarsPath, &inputs); err != nil {
		return fmt.Errorf("substrate_plan: %w", cp9errors.Newf(cp9errors.CategoryResource, "write terraform tfvars: %v", err))
	}
	runner := s.Runner
	runner.AllowEKSPlan = s.Session.Target.AWS.RemoteEKS.Enabled
	if _, err := runner.Plan(ctx, &paths); err != nil {
		return fmt.Errorf("substrate_plan: %w", err)
	}
	return nil
}

func substrateTags(session *config.SessionConfig, awsState *cpaws.SessionState) (map[string]string, error) {
	if session == nil {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "terraform tags require session config")
	}
	if session.Host.TTL.Duration <= 0 {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "host.ttl must be positive so terraform resources receive cp9-ttl")
	}
	if awsState == nil {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "terraform tags require persisted aws session state")
	}
	if awsState.CreatedAt.IsZero() {
		return nil, cp9errors.New(cp9errors.CategoryConfig, "terraform tags require aws session state created_at")
	}
	tags := make(map[string]string, len(awsState.SessionTags)+2)
	maps.Copy(tags, awsState.SessionTags)
	tags[cpaws.TagManaged] = cpaws.TagManagedTerraformSubstrate
	tags[cpaws.TagTTL] = awsState.CreatedAt.UTC().Add(session.Host.TTL.Duration).Format(time.RFC3339)
	return tags, nil
}

// SubstrateApplyStep applies the substrate and persists raw + typed outputs for later steps.
type SubstrateApplyStep struct {
	StateDir string
	Session  *config.SessionConfig
	Runner   cpterraform.Runner
}

func (s *SubstrateApplyStep) Name() cluster.StepName { return cluster.StepSubstrateApply }

func (s *SubstrateApplyStep) Run(_ context.Context) error {
	return fmt.Errorf("substrate_apply requires lifecycle state")
}

func (s *SubstrateApplyStep) RunWithState(ctx context.Context, _ *cluster.State) error {
	if s.Session == nil {
		return cp9errors.New(cp9errors.CategoryConfig, "substrate_apply: session config is required")
	}
	paths := cpterraform.WorkspacePathsForVarFile(s.StateDir, s.Session.Terraform.VarFile)
	if err := refreshSubstrateSourceIfConfigured(s.Session, paths.SourceDir); err != nil {
		return fmt.Errorf("substrate_apply: refresh terraform source: %w", err)
	}
	runner := s.Runner
	runner.AllowEKSPlan = s.Session.Target.AWS.RemoteEKS.Enabled
	if _, err := runner.Plan(ctx, &paths); err != nil {
		return fmt.Errorf("substrate_apply: refresh terraform plan: %w", err)
	}
	outputData, err := runner.Apply(ctx, &paths)
	if err != nil {
		return fmt.Errorf("substrate_apply: %w", err)
	}
	outputs, contract, err := cpterraform.ParseOutputs(outputData)
	if err != nil {
		return fmt.Errorf("substrate_apply: %w", err)
	}
	if err := cpterraform.WriteOutputs(paths.OutputsPath, outputs); err != nil {
		return fmt.Errorf("substrate_apply: %w", cp9errors.Newf(cp9errors.CategoryResource, "persist terraform outputs: %v", err))
	}
	if err := cpterraform.WriteContract(paths.ContractPath, contract); err != nil {
		return fmt.Errorf("substrate_apply: %w", err)
	}
	if err := persistRemoteEKSArtifacts(s.StateDir, contract.RemoteEKS); err != nil {
		return fmt.Errorf("substrate_apply: %w", err)
	}
	if err := persistO11yStubArtifacts(s.StateDir, contract.O11yStub); err != nil {
		return fmt.Errorf("substrate_apply: %w", err)
	}
	return nil
}

func refreshSubstrateSourceIfConfigured(session *config.SessionConfig, sourceDir string) error {
	if session == nil {
		return nil
	}
	if strings.TrimSpace(session.Terraform.Repo) == "" || strings.TrimSpace(session.Terraform.Path) == "" {
		return nil
	}
	return cpterraform.MaterializeSource(session.Terraform.Repo, session.Terraform.Path, sourceDir)
}

func persistRemoteEKSArtifacts(stateDir string, remote *cpterraform.RemoteEKSOutput) error {
	if remote == nil || !remote.Enabled {
		return nil
	}
	if strings.TrimSpace(stateDir) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "remote EKS artifact state dir is required")
	}
	if strings.TrimSpace(remote.ClusterName) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "remote EKS cluster_name is required")
	}
	if strings.TrimSpace(remote.Endpoint) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "remote EKS endpoint is required")
	}
	if strings.TrimSpace(remote.CertificateAuthorityData) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "remote EKS certificate_authority_data is required")
	}
	dir := filepath.Join(stateDir, "remote-eks")
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "create remote EKS artifact dir: %v", err)
	}
	data, err := json.MarshalIndent(remote, "", "  ")
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryConfig, "marshal remote EKS artifact: %v", err)
	}
	if err := os.WriteFile(filepath.Join(dir, "remote-eks.json"), data, 0o600); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "write remote EKS artifact: %v", err)
	}
	if err := os.WriteFile(filepath.Join(dir, "kubeconfig.yaml"), []byte(remoteEKSKubeconfig(remote)), 0o600); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "write remote EKS kubeconfig: %v", err)
	}
	return nil
}

func remoteEKSKubeconfig(remote *cpterraform.RemoteEKSOutput) string {
	contextName := strings.TrimSpace(remote.KubeconfigContext)
	if contextName == "" {
		contextName = strings.TrimSpace(remote.ClusterName)
	}
	region := strings.TrimSpace(remote.Region)
	var b strings.Builder
	fmt.Fprintf(&b, "apiVersion: v1\n")
	fmt.Fprintf(&b, "kind: Config\n")
	fmt.Fprintf(&b, "clusters:\n")
	fmt.Fprintf(&b, "- name: %s\n", contextName)
	fmt.Fprintf(&b, "  cluster:\n")
	fmt.Fprintf(&b, "    server: %s\n", strings.TrimSpace(remote.Endpoint))
	fmt.Fprintf(&b, "    certificate-authority-data: %s\n", strings.TrimSpace(remote.CertificateAuthorityData))
	fmt.Fprintf(&b, "contexts:\n")
	fmt.Fprintf(&b, "- name: %s\n", contextName)
	fmt.Fprintf(&b, "  context:\n")
	fmt.Fprintf(&b, "    cluster: %s\n", contextName)
	fmt.Fprintf(&b, "    user: %s\n", contextName)
	fmt.Fprintf(&b, "current-context: %s\n", contextName)
	fmt.Fprintf(&b, "users:\n")
	fmt.Fprintf(&b, "- name: %s\n", contextName)
	fmt.Fprintf(&b, "  user:\n")
	fmt.Fprintf(&b, "    exec:\n")
	fmt.Fprintf(&b, "      apiVersion: client.authentication.k8s.io/v1beta1\n")
	fmt.Fprintf(&b, "      command: aws\n")
	fmt.Fprintf(&b, "      args:\n")
	fmt.Fprintf(&b, "      - eks\n")
	fmt.Fprintf(&b, "      - get-token\n")
	fmt.Fprintf(&b, "      - --cluster-name\n")
	fmt.Fprintf(&b, "      - %s\n", strings.TrimSpace(remote.ClusterName))
	if region != "" {
		fmt.Fprintf(&b, "      - --region\n")
		fmt.Fprintf(&b, "      - %s\n", region)
	}
	return b.String()
}

func persistO11yStubArtifacts(stateDir string, stub *cpterraform.O11yStubOutput) error {
	if stub == nil || !stub.Enabled {
		return nil
	}
	if strings.TrimSpace(stateDir) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "o11y stub artifact state dir is required")
	}
	if strings.TrimSpace(stub.EndpointURL) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "o11y stub endpoint_url is required")
	}
	if strings.TrimSpace(stub.Token) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "o11y stub token is required")
	}
	if strings.TrimSpace(stub.EvidenceTable) == "" {
		return cp9errors.New(cp9errors.CategoryConfig, "o11y stub evidence_table is required")
	}
	if err := os.MkdirAll(cluster.O11yDir(stateDir), 0o755); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "create o11y artifact dir: %v", err)
	}
	data, err := json.MarshalIndent(stub, "", "  ")
	if err != nil {
		return cp9errors.Newf(cp9errors.CategoryConfig, "marshal o11y stub artifact: %v", err)
	}
	if err := os.WriteFile(cluster.O11yStubPath(stateDir), data, 0o600); err != nil {
		return cp9errors.Newf(cp9errors.CategoryResource, "write o11y stub artifact: %v", err)
	}
	return nil
}
