package lifecycle

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"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/dbaasui"
	"github.com/tidbcloud/controlplane9/internal/host"
	cpterraform "github.com/tidbcloud/controlplane9/internal/terraform"
	cptilt "github.com/tidbcloud/controlplane9/internal/tilt"
)

const sessionContextEvidenceDirName = "evidence"

// SessionContextWriteStep writes the generic cp9-owned facts consumed by a
// user-owned Session Tiltfile. It deliberately does not emit persona services,
// manifests, probes, or build scripts.
type SessionContextWriteStep struct {
	StateDir string
}

func (s *SessionContextWriteStep) Name() cluster.StepName { return cluster.StepSessionContextWrite }

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

func (s *SessionContextWriteStep) RunWithState(_ context.Context, state *cluster.State) error {
	if strings.TrimSpace(s.StateDir) == "" {
		return fmt.Errorf("session_context_write: state dir is required")
	}
	if state == nil {
		return fmt.Errorf("session_context_write: lifecycle state is required")
	}
	resolved, err := config.LoadSession(resolvedSessionPathForState(s.StateDir, state))
	if err != nil {
		return fmt.Errorf("session_context_write: load resolved session: %w", err)
	}
	if !config.UsesSessionTiltfile(resolved) {
		return nil
	}
	paths := cptilt.PathsForState(s.StateDir, state)
	selected := host.ResolveSessionConfig(resolved)
	if loaded, err := host.Load(s.StateDir); err == nil {
		selected = loaded
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("session_context_write: load selected host: %w", err)
	}
	evidenceDir := filepath.Join(paths.ReportsDir, sessionContextEvidenceDirName)
	absKubeconfigPath := absSessionContextPath(selected.KubeconfigPath)
	absTiltfilePath := absSessionContextPath(paths.Tiltfile)
	absReportsDir := absSessionContextPath(paths.ReportsDir)
	absEvidenceDir := absSessionContextPath(evidenceDir)
	absResolvedSessionPath := absSessionContextPath(resolvedSessionPathForState(s.StateDir, state))
	terraformPaths := cpterraform.WorkspacePathsForVarFile(s.StateDir, resolved.Terraform.VarFile)
	terraformOutputsPath := terraformPaths.OutputsPath
	terraformContractPath := terraformPaths.ContractPath
	if state.Paths != nil {
		if strings.TrimSpace(state.Paths.TerraformOutputs) != "" {
			terraformOutputsPath = state.Paths.TerraformOutputs
		}
		if strings.TrimSpace(state.Paths.TerraformContract) != "" {
			terraformContractPath = state.Paths.TerraformContract
		}
	}
	var awsState *cpaws.SessionState
	targetAccount := ""
	if loaded, err := cpaws.LoadSessionState(s.StateDir); err == nil {
		awsState = loaded
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("session_context_write: load aws state: %w", err)
	} else if config.UsesCloudSubstrate(resolved) {
		return fmt.Errorf("session_context_write: persisted AWS session state is required for a cloud Session")
	}
	if awsState != nil {
		targetAccount = strings.TrimSpace(awsState.Account)
	}
	if config.UsesCloudSubstrate(resolved) && targetAccount == "" {
		return fmt.Errorf("session_context_write: persisted AWS session state is missing account")
	}
	sessionTags := map[string]string{}
	ttlExpiresAt := ""
	if awsState != nil {
		for key, value := range awsState.SessionTags {
			key = strings.TrimSpace(key)
			value = strings.TrimSpace(value)
			if key == "" || value == "" {
				continue
			}
			sessionTags[key] = value
		}
		if resolved.Host.TTL.Duration > 0 && !awsState.CreatedAt.IsZero() {
			ttlExpiresAt = awsState.CreatedAt.UTC().Add(resolved.Host.TTL.Duration).Format(time.RFC3339)
		}
	}
	var iam *cpterraform.IAMOutput
	var metering *cpterraform.MeteringOutput
	var remoteEC2Runtime *cpterraform.RemoteEC2RuntimeOutput
	var sessionCMK *cpterraform.SessionCMK
	var sessionProductPCA *cpterraform.SessionProductPCA
	var dedicatedPublicDNS *cpterraform.DedicatedPublicDNS
	if contract, err := cpterraform.LoadOutputContract(terraformContractPath); err == nil && contract != nil {
		iam = contract.IAM
		metering = contract.Metering
		remoteEC2Runtime = contract.RemoteEC2Runtime
		sessionCMK = contract.SessionCMK
		sessionProductPCA = contract.SessionProductPCA
		dedicatedPublicDNS = contract.DedicatedPublicDNS
	} else if err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("session_context_write: load terraform contract: %w", err)
	}
	for _, dir := range []string{
		paths.TiltDir,
		paths.ReportsDir,
		filepath.Dir(paths.ContextIndex),
		evidenceDir,
	} {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("session_context_write: create %s: %w", dir, err)
		}
	}

	runtimeID := ""
	if config.DedicatedGCPEnabled(resolved) {
		runtimeID = cptilt.RuntimePrimary
	}
	report := &cptilt.ContextReport{
		RuntimeID:      runtimeID,
		SessionName:    resolved.Name,
		Owner:          state.Owner,
		Namespace:      selected.Namespace,
		Mode:           resolved.Tilt.Mode,
		KubeContext:    selected.Context,
		KubeconfigPath: cptilt.SessionRelativePath(s.StateDir, absKubeconfigPath),
		ContextPath:    cptilt.SessionRelativePath(s.StateDir, paths.ContextIndex),
		TiltfilePath:   cptilt.SessionRelativePath(s.StateDir, paths.Tiltfile),
	}
	contextHost := sessionTiltfileContextHostFromSelected(selected)
	if config.DedicatedGCPEnabled(resolved) {
		regional, err := host.Load(host.GCPRegionalStateDir(s.StateDir))
		if err != nil {
			return fmt.Errorf("session_context_write: load GCP regional host: %w", err)
		}
		regionalContext := sessionTiltfileContextHostFromSelected(regional)
		contextHost.Regional = &regionalContext
	}
	var uiContext *sessionTiltfileContextUI
	if config.SessionPersona(resolved) == "dev-full" {
		uiContext = sessionTiltfileUIContextFromSelected(selected)
	}
	runtimeConfigPath := filepath.Join(filepath.Dir(paths.ContextIndex), "dbaas-ui-config.js")
	if uiContext != nil {
		runtimeConfig, err := dbaasui.RenderRuntimeConfig(SessionUIO11YRuntimeConfig(resolved.UI.O11Y))
		if err != nil {
			return fmt.Errorf("session_context_write: render dbaas-ui runtime config: %w", err)
		}
		if err := os.WriteFile(runtimeConfigPath, runtimeConfig, 0o600); err != nil {
			return fmt.Errorf("session_context_write: write dbaas-ui runtime config: %w", err)
		}
		uiContext.RuntimeConfigPath = absSessionContextPath(runtimeConfigPath)
	} else if err := os.Remove(runtimeConfigPath); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("session_context_write: remove unsupported dbaas-ui runtime config: %w", err)
	}
	contextIndex := sessionTiltfileContextIndex{
		RuntimeID:              runtimeID,
		CreatedAt:              state.CreatedAt.UTC().Format(time.RFC3339),
		SessionName:            resolved.Name,
		Owner:                  state.Owner,
		Namespace:              selected.Namespace,
		KubeContext:            selected.Context,
		KubeconfigPath:         absKubeconfigPath,
		TiltfilePath:           absTiltfilePath,
		ReportsDir:             absReportsDir,
		EvidenceDir:            absEvidenceDir,
		ResolvedSessionPath:    absResolvedSessionPath,
		SessionTags:            sessionTags,
		TTLExpiresAt:           ttlExpiresAt,
		SourceBuildSupported:   true,
		SourceBuildParallelism: resolved.Services.SourceBuildParallelism,
		GitHubAppClientID:      resolved.Services.GitHubAppClientID,
		EssentialV2: sessionContextEssentialV2{
			Enabled:                  config.EssentialV2Enabled(resolved),
			EnabledTiKVRemoteWorkers: config.EssentialV2EnabledTiKVRemoteWorkers(resolved),
		},
		Target: sessionTiltfileContextTarget{
			Cloud:   resolved.Target.Cloud,
			Region:  resolved.Target.Region,
			Account: targetAccount,
		},
		Terraform: sessionTiltfileContextTerraform{
			OutputsPath:        absSessionContextPath(terraformOutputsPath),
			ContractPath:       absSessionContextPath(terraformContractPath),
			IAM:                iam,
			Metering:           metering,
			RemoteEC2Runtime:   remoteEC2Runtime,
			SessionCMK:         sessionCMK,
			SessionProductPCA:  sessionProductPCA,
			DedicatedPublicDNS: dedicatedPublicDNS,
		},
		Host: contextHost,
		UI:   uiContext,
	}
	if environment := strings.TrimSpace(resolved.Artifacts.Environment); environment != "" {
		snapshotDir := artifactsync.OutputDir(s.StateDir)
		snapshot, err := artifactsync.LoadSnapshot(snapshotDir)
		if err != nil {
			return fmt.Errorf("session_context_write: load artifact snapshot: %w", err)
		}
		if snapshot.Environment != environment {
			return fmt.Errorf(
				"session_context_write: artifact snapshot environment %s does not match resolved session %s",
				snapshot.Environment,
				environment,
			)
		}
		artifacts := &sessionTiltfileContextArtifacts{
			SnapshotPath: absSessionContextPath(filepath.Join(snapshotDir, artifactsync.SnapshotFile)),
		}
		contextIndex.Artifacts = artifacts
	}
	if err := writeJSONFile(paths.ContextIndex, contextIndex); err != nil {
		return fmt.Errorf("session_context_write: write context index: %w", err)
	}
	if err := writeJSONFile(paths.ContextReport, report); err != nil {
		return fmt.Errorf("session_context_write: write context report: %w", err)
	}
	if config.DedicatedGCPEnabled(resolved) {
		if err := writeGCPRuntimeContext(s.StateDir, resolved, &contextIndex, &paths, report); err != nil {
			return fmt.Errorf("session_context_write: %w", err)
		}
	}
	if state.Paths != nil {
		state.Paths.ContextDir = filepath.Dir(paths.ContextIndex)
		state.Paths.TiltDir = paths.TiltDir
		state.Paths.Tiltfile = paths.Tiltfile
	}
	return nil
}

// SessionUIO11YRuntimeConfig maps the persisted Session schema to the exact
// browser runtime payload shared by materialization, readiness, and status.
func SessionUIO11YRuntimeConfig(input *config.SessionUIO11YConfig) *dbaasui.O11YRuntimeConfig {
	if input == nil {
		return nil
	}
	return &dbaasui.O11YRuntimeConfig{
		GSHost:               input.GSHost,
		GSHostV2:             input.GSHostV2,
		TestHostDomainSuffix: input.TestHostDomainSuffix,
		NextGenBizType:       input.NextGenBizType,
		DedicatedBizType:     input.DedicatedBizType,
		DevTierBizType:       input.DevTierBizType,
	}
}

func sessionTiltfileUIContextFromSelected(selected *host.Session) *sessionTiltfileContextUI {
	if selected == nil || selected.Mode != config.HostModeRemoteEC2K3d {
		return nil
	}
	return &sessionTiltfileContextUI{}
}

func absSessionContextPath(path string) string {
	if strings.TrimSpace(path) == "" {
		return ""
	}
	abs, err := filepath.Abs(path)
	if err != nil {
		return path
	}
	return abs
}

func sessionTiltfileRemoteContext(remote *host.RemoteSession) *host.RemoteSession {
	if remote == nil {
		return nil
	}
	out := *remote
	out.SSHPrivateKeyPath = absSessionContextPath(out.SSHPrivateKeyPath)
	return &out
}

func sessionTiltfileContextHostFromSelected(selected *host.Session) sessionTiltfileContextHost {
	if selected == nil {
		return sessionTiltfileContextHost{}
	}
	var gcp *host.GCPRemoteSession
	if selected.GCP != nil {
		gcpCopy := *selected.GCP
		if strings.TrimSpace(gcpCopy.AWSRoleARN) != "" {
			gcpCopy.AWSWebIdentityTokenFile = host.GCPAWSWebIdentityTokenNodePath
		}
		gcp = &gcpCopy
	}
	return sessionTiltfileContextHost{
		Controller:     selected.ExecutionFacts().Controller,
		Mode:           string(selected.Mode),
		StateDir:       absSessionContextPath(selected.StateDir),
		ClusterName:    selected.ClusterName,
		Namespace:      selected.Namespace,
		KubeconfigPath: absSessionContextPath(selected.KubeconfigPath),
		Context:        selected.Context,
		Remote:         sessionTiltfileRemoteContext(selected.Remote),
		DedicatedAPI:   sessionTiltfileDedicatedAPIContext(selected.DedicatedAPI),
		GCP:            gcp,
	}
}

func sessionTiltfileDedicatedAPIContext(in *host.DedicatedAPISession) *host.DedicatedAPISession {
	if in == nil {
		return nil
	}
	out := *in
	out.AdminKubeconfigPath = absSessionContextPath(out.AdminKubeconfigPath)
	out.InternalKubeconfigPath = absSessionContextPath(out.InternalKubeconfigPath)
	out.PKIDir = absSessionContextPath(out.PKIDir)
	out.HostManifestPath = absSessionContextPath(out.HostManifestPath)
	out.BootstrapManifestPath = absSessionContextPath(out.BootstrapManifestPath)
	return &out
}

type sessionTiltfileContextIndex struct {
	RuntimeID              string                           `json:"runtime_id,omitempty"`
	CreatedAt              string                           `json:"created_at"`
	SessionName            string                           `json:"session_name"`
	Owner                  string                           `json:"owner"`
	Namespace              string                           `json:"namespace"`
	KubeContext            string                           `json:"kube_context"`
	KubeconfigPath         string                           `json:"kubeconfig_path"`
	TiltfilePath           string                           `json:"tiltfile_path"`
	ReportsDir             string                           `json:"reports_dir"`
	EvidenceDir            string                           `json:"evidence_dir"`
	ResolvedSessionPath    string                           `json:"resolved_session_path"`
	SessionTags            map[string]string                `json:"session_tags,omitempty"`
	TTLExpiresAt           string                           `json:"ttl_expires_at,omitempty"`
	SourceBuildSupported   bool                             `json:"source_build_supported"`
	SourceBuildParallelism int                              `json:"source_build_parallelism"`
	GitHubAppClientID      string                           `json:"github_app_client_id"`
	EssentialV2            sessionContextEssentialV2        `json:"essential_v2"`
	Target                 sessionTiltfileContextTarget     `json:"target"`
	Terraform              sessionTiltfileContextTerraform  `json:"terraform,omitzero"`
	Host                   sessionTiltfileContextHost       `json:"host"`
	UI                     *sessionTiltfileContextUI        `json:"ui,omitempty"`
	Artifacts              *sessionTiltfileContextArtifacts `json:"artifacts,omitempty"`
}

func writeGCPRuntimeContext(
	stateDir string,
	resolved *config.SessionConfig,
	primary *sessionTiltfileContextIndex,
	primaryPaths *cptilt.Paths,
	primaryReport *cptilt.ContextReport,
) error {
	gcpStateDir := host.GCPRegionalStateDir(stateDir)
	selected, err := host.Load(gcpStateDir)
	if err != nil {
		return fmt.Errorf("load GCP regional host: %w", err)
	}
	paths := cptilt.PathsForState(gcpStateDir, nil)
	paths.Tiltfile = primaryPaths.Tiltfile
	evidenceDir := filepath.Join(paths.ReportsDir, sessionContextEvidenceDirName)
	for _, dir := range []string{paths.TiltDir, paths.ReportsDir, filepath.Dir(paths.ContextIndex), evidenceDir} {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("create GCP runtime path %s: %w", dir, err)
		}
	}

	contextIndex := *primary
	contextIndex.RuntimeID = cptilt.RuntimeGCPRegional
	contextIndex.Namespace = selected.Namespace
	contextIndex.KubeContext = selected.Context
	contextIndex.KubeconfigPath = absSessionContextPath(selected.KubeconfigPath)
	contextIndex.TiltfilePath = absSessionContextPath(paths.Tiltfile)
	contextIndex.ReportsDir = absSessionContextPath(paths.ReportsDir)
	contextIndex.EvidenceDir = absSessionContextPath(evidenceDir)
	contextIndex.SourceBuildSupported = false
	contextIndex.Target = sessionTiltfileContextTarget{
		Cloud:  "gcp",
		Region: resolved.Dedicated.GCP.Region,
	}
	contextIndex.Terraform = sessionTiltfileContextTerraform{
		SessionProductPCA:  primary.Terraform.SessionProductPCA,
		DedicatedPublicDNS: primary.Terraform.DedicatedPublicDNS,
	}
	contextIndex.Host = sessionTiltfileContextHostFromSelected(selected)
	if primary.Host.Remote == nil {
		return fmt.Errorf("write GCP runtime context: primary remote host is required")
	}
	contextIndex.Host.Primary = &sessionTiltfilePrimaryHostContext{
		Remote: sessionTiltfilePrimaryRemoteContext{
			Region:         primary.Host.Remote.Region,
			PublicIP:       primary.Host.Remote.PublicIP,
			PublicHTTPSURL: primary.Host.Remote.PublicHTTPSURL,
		},
	}
	contextIndex.UI = nil

	report := &cptilt.ContextReport{
		RuntimeID:      cptilt.RuntimeGCPRegional,
		SessionName:    primaryReport.SessionName,
		Owner:          primaryReport.Owner,
		Namespace:      selected.Namespace,
		Mode:           primaryReport.Mode,
		KubeContext:    selected.Context,
		KubeconfigPath: cptilt.SessionRelativePath(gcpStateDir, selected.KubeconfigPath),
		ContextPath:    cptilt.SessionRelativePath(gcpStateDir, paths.ContextIndex),
		TiltfilePath:   cptilt.SessionRelativePath(gcpStateDir, paths.Tiltfile),
	}
	if err := writeJSONFile(paths.ContextIndex, contextIndex); err != nil {
		return fmt.Errorf("write GCP runtime context index: %w", err)
	}
	if err := writeJSONFile(paths.ContextReport, report); err != nil {
		return fmt.Errorf("write GCP runtime context report: %w", err)
	}
	return nil
}

type sessionTiltfileContextArtifacts struct {
	SnapshotPath string `json:"snapshot_path"`
}

type sessionTiltfileContextUI struct {
	RuntimeConfigPath string `json:"runtime_config_path"`
}

type sessionTiltfileContextTarget struct {
	Cloud   string `json:"cloud"`
	Region  string `json:"region"`
	Account string `json:"account,omitempty"`
}

type sessionTiltfileContextTerraform struct {
	OutputsPath        string                              `json:"outputs_path,omitempty"`
	ContractPath       string                              `json:"contract_path,omitempty"`
	IAM                *cpterraform.IAMOutput              `json:"iam,omitempty"`
	Metering           *cpterraform.MeteringOutput         `json:"metering,omitempty"`
	RemoteEC2Runtime   *cpterraform.RemoteEC2RuntimeOutput `json:"remote_ec2_runtime,omitempty"`
	SessionCMK         *cpterraform.SessionCMK             `json:"session_cmk,omitempty"`
	SessionProductPCA  *cpterraform.SessionProductPCA      `json:"session_product_pca,omitempty"`
	DedicatedPublicDNS *cpterraform.DedicatedPublicDNS     `json:"dedicated_public_dns,omitempty"`
}

type sessionTiltfileContextHost struct {
	Controller     string                             `json:"controller"`
	Mode           string                             `json:"mode"`
	StateDir       string                             `json:"state_dir"`
	ClusterName    string                             `json:"cluster_name"`
	Namespace      string                             `json:"namespace"`
	KubeconfigPath string                             `json:"kubeconfig_path"`
	Context        string                             `json:"context"`
	Remote         *host.RemoteSession                `json:"remote,omitempty"`
	DedicatedAPI   *host.DedicatedAPISession          `json:"dedicated_api,omitempty"`
	GCP            *host.GCPRemoteSession             `json:"gcp,omitempty"`
	Local          *sessionTiltfileContextHost        `json:"local,omitempty"`
	Primary        *sessionTiltfilePrimaryHostContext `json:"primary,omitempty"`
	Regional       *sessionTiltfileContextHost        `json:"regional,omitempty"`
}

type sessionTiltfilePrimaryHostContext struct {
	Remote sessionTiltfilePrimaryRemoteContext `json:"remote"`
}

type sessionTiltfilePrimaryRemoteContext struct {
	Region         string `json:"region"`
	PublicIP       string `json:"public_ip"`
	PublicHTTPSURL string `json:"public_https_url"`
}

// sessionContextEssentialV2 is the Essential v2 shape written into the session
// context.
type sessionContextEssentialV2 struct {
	Enabled                  bool     `json:"enabled"`
	EnabledTiKVRemoteWorkers []string `json:"enabled_tikv_remote_workers"`
}
