package kustomize

import (
	"encoding/json"
	"fmt"
	"log"
	"maps"
	"os"
	"path"
	"path/filepath"
	"slices"
	"strings"

	"gopkg.in/yaml.v3"

	"github.com/tidbcloud/controlplane9/internal/persona"
	"github.com/tidbcloud/controlplane9/internal/stub"
)

// defaultNamespace is used when no namespace is specified.
const defaultNamespace = "default"

// Well-known environment variable names for database DSN configuration.
// The overlay generator patches these when found in staging configs.
var dsnEnvVars = []string{
	"TIDB_DSN",
	"DATABASE_URL",
	"DB_DSN",
	"MYSQL_DSN",
}

// Well-known environment variable names for AWS configuration.
var awsEnvVars = map[string]bool{
	"AWS_PROFILE":    true,
	"AWS_REGION":     true,
	"AWS_ACCESS_KEY": true,
	"AWS_SECRET_KEY": true,
}

const (
	cp9OwnerEnv   = "CP9_OWNER"
	cp9SessionEnv = "CP9_SESSION"
)

// Generate creates Kustomize overlays for all non-skip services in the plan.
// It writes per-service overlay directories under outputDir.
func Generate(input *OverlayInput, outputDir string) (*GenerateResult, error) {
	if err := os.MkdirAll(outputDir, 0o755); err != nil {
		return nil, fmt.Errorf("create output dir: %w", err)
	}

	ns := input.Namespace
	if ns == "" {
		ns = defaultNamespace
	}

	var generated []string
	var skipped []string
	overlays := make(map[string]OverlayResult)

	if input.Plan == nil {
		return &GenerateResult{OutputDir: outputDir}, nil
	}

	// Sort service plans for deterministic output.
	services := make([]persona.ServicePlan, len(input.Plan.Services))
	copy(services, input.Plan.Services)
	slices.SortFunc(services, func(a, b persona.ServicePlan) int {
		return strings.Compare(a.Name, b.Name)
	})

	// Precompute sorted non-SKIP service names (longest first) for endpoint rewriting.
	sortedSvcNames := buildSortedServiceNames(input)

	for i := range services {
		svc := &services[i]
		if svc.Role == persona.RoleSkip {
			skipped = append(skipped, svc.Name)
			continue
		}

		overlay := buildServiceOverlay(input, svc, ns, sortedSvcNames)

		svcDir := filepath.Join(outputDir, svc.Name)
		if err := writeOverlay(svcDir, overlay); err != nil {
			return nil, fmt.Errorf("write overlay for %s: %w", svc.Name, err)
		}

		generated = append(generated, svc.Name)
		overlays[svc.Name] = OverlayResult{
			Name:          svc.Name,
			Role:          svc.Role,
			Directory:     svcDir,
			Namespace:     overlay.Namespace,
			Image:         overlay.Image,
			WorkloadKind:  overlay.WorkloadKind,
			WorkloadName:  overlay.WorkloadName,
			ContainerName: overlay.ContainerName,
			Ports:         clonePorts(overlay.Ports),
			Synthetic:     overlay.Synthetic,
		}
		log.Printf("kustomize: generated overlay for %s (%s)", svc.Name, svc.Role)
	}

	return &GenerateResult{
		OutputDir: outputDir,
		Services:  generated,
		Skipped:   skipped,
		Overlays:  overlays,
	}, nil
}

// buildSortedServiceNames returns non-SKIP service names sorted longest-first
// for deterministic endpoint rewriting.
func buildSortedServiceNames(input *OverlayInput) []string {
	if input.Plan == nil {
		return nil
	}
	var names []string
	for _, svc := range input.Plan.Services {
		if svc.Role != persona.RoleSkip {
			names = append(names, svc.Name)
		}
	}
	slices.SortFunc(names, func(a, b string) int {
		if len(b) != len(a) {
			return len(b) - len(a) // longest first
		}
		return strings.Compare(a, b)
	})
	return names
}

// buildServiceOverlay creates the overlay for a single service.
func buildServiceOverlay(input *OverlayInput, svc *persona.ServicePlan, ns string, sortedSvcNames []string) *ServiceOverlay {
	overlay := &ServiceOverlay{
		Name:          svc.Name,
		Role:          svc.Role,
		Namespace:     ns,
		WorkloadKind:  "Deployment",
		WorkloadName:  svc.Name,
		ContainerName: svc.Name,
		EnvPatches:    make(map[string]string),
		Ports:         make(map[string]int),
	}

	// If we have staging config, use its base/workload metadata, but keep the
	// generated overlay in the target session namespace.
	if sc, ok := input.StagingConfigs[svc.Name]; ok {
		if sc.KustomizePath != "" {
			if input.StagingRoot != "" {
				overlay.KustomizeBase = filepath.Join(input.StagingRoot, sc.KustomizePath)
			} else {
				overlay.KustomizeBase = sc.KustomizePath
			}
		}
		if sc.WorkloadKind != "" {
			overlay.WorkloadKind = sc.WorkloadKind
		}
		if sc.WorkloadName != "" {
			overlay.WorkloadName = sc.WorkloadName
		}
		if sc.ContainerName != "" {
			overlay.ContainerName = sc.ContainerName
		}
		if sc.Image != "" {
			overlay.Image = sc.Image
		}
		if len(sc.Ports) > 0 {
			overlay.Ports = clonePorts(sc.Ports)
		}
	}
	if overlay.KustomizeBase == "" {
		overlay.Synthetic = true
	}
	if image, ok := input.Images[svc.Name]; ok && image != "" {
		overlay.Image = image
	}
	if overlay.Image == "" {
		overlay.Image = fmt.Sprintf("cp9.local/%s:latest", svc.Name)
	}
	if len(overlay.Ports) == 0 {
		overlay.Ports = defaultPortsForService(svc.Role)
	}

	// Patch DB DSN only for DSN env vars already present in the staging config.
	// If no staging config exists, patch all known DSN vars as a fallback.
	if dsn, ok := input.DSNs[svc.Name]; ok && dsn != "" {
		if staging, ok := input.StagingConfigs[svc.Name]; ok && len(staging.Env) > 0 {
			for envKey := range staging.Env {
				if isDSNEnvVar(envKey) {
					overlay.EnvPatches[envKey] = dsn
				}
			}
		} else {
			for _, envVar := range dsnEnvVars {
				overlay.EnvPatches[envVar] = dsn
			}
		}
	}

	// Patch AWS config.
	if input.AWS.Profile != "" {
		overlay.EnvPatches["AWS_PROFILE"] = input.AWS.Profile
	}
	if input.AWS.Region != "" {
		overlay.EnvPatches["AWS_REGION"] = input.AWS.Region
	}
	if input.Owner != "" {
		overlay.EnvPatches[cp9OwnerEnv] = input.Owner
	}
	if input.Session != "" {
		overlay.EnvPatches[cp9SessionEnv] = input.Session
	}
	maps.Copy(overlay.EnvPatches, svc.ExtraEnv)
	applyRecordingStubSettings(overlay, svc)

	// Patch service endpoint env vars — rewrite to k3d DNS.
	// Use overlay.Namespace so endpoints match where services are deployed.
	if staging, ok := input.StagingConfigs[svc.Name]; ok {
		for envKey, envVal := range staging.Env {
			if envKey == "AWS_PROFILE" || envKey == "AWS_REGION" {
				continue // handled above
			}
			if awsEnvVars[envKey] {
				// Clear staging AWS credential vars (e.g. AWS_ACCESS_KEY, AWS_SECRET_KEY)
				// so they don't leak into the local environment.
				log.Printf("kustomize: clearing staging AWS credential var %q for service %q", envKey, svc.Name)
				overlay.EnvPatches[envKey] = ""
				continue
			}
			if isDSNEnvVar(envKey) {
				continue // handled above
			}
			// Check if the value looks like a service endpoint (host:port or DNS name).
			if rewritten := rewriteEndpoint(envVal, sortedSvcNames, overlay.Namespace); rewritten != envVal {
				overlay.EnvPatches[envKey] = rewritten
			}
		}
	}

	return overlay
}

func defaultPortsForService(role persona.Role) map[string]int {
	ports := map[string]int{"http": 8080}
	if role == persona.RoleStub {
		ports["grpc"] = 50051
	}
	return ports
}

func applyRecordingStubSettings(overlay *ServiceOverlay, svc *persona.ServicePlan) {
	if svc == nil || svc.Role != persona.RoleStub || svc.Stub == nil {
		return
	}
	service := svc.Name
	kind := stub.Kind(strings.TrimSpace(svc.Stub.Kind))
	if !stub.IsRecordingKind(kind) {
		return
	}

	if recordFile := stub.RecordingContainerFile(kind); recordFile != "" {
		overlay.EnvPatches[stub.EnvRecordFile] = recordFile
	}
	if kind == stub.KindMeteringRecorder {
		overlay.EnvPatches[stub.EnvBillingEndpoint] = "http://" +
			K3dServiceDNS("billing-svc", overlay.Namespace, stub.DefaultHTTPPort) +
			stub.BillingRecordRoute
	}

	overlay.Volumes = append(overlay.Volumes, VolumeSpec{
		Name:     "cp9-recording-state",
		HostPath: path.Join(stub.RecordingNodeServiceDir(service)),
	})
	overlay.VolumeMounts = append(overlay.VolumeMounts, VolumeMount{
		Name:      "cp9-recording-state",
		MountPath: stub.RecordingMountPath,
	})
}

// rewriteEndpoint checks if a value contains a service name reference
// and rewrites it to k3d DNS format. sortedSvcNames must be pre-sorted
// longest-first to avoid partial matches and ensure deterministic output.
func rewriteEndpoint(value string, sortedSvcNames []string, ns string) string {
	for _, svcName := range sortedSvcNames {
		if containsServiceRef(value, svcName) {
			return rewriteToK3dDNS(value, svcName, ns)
		}
	}
	return value
}

// containsServiceRef checks if a value contains a reference to the given service.
func containsServiceRef(value, serviceName string) bool {
	// Normalize: service names may use hyphens or underscores.
	normalized := strings.ReplaceAll(serviceName, "_", "-")
	return strings.Contains(value, normalized) || strings.Contains(value, serviceName)
}

// rewriteToK3dDNS rewrites a service reference to k3d internal DNS.
// Input: "billing-svc:8080" or "billing-svc.staging:8080"
// Output: "billing-svc.default.svc.cluster.local:8080"
func rewriteToK3dDNS(value, serviceName, ns string) string {
	// Kubernetes DNS labels must be DNS-safe, so normalize underscores to hyphens.
	normalized := strings.ReplaceAll(serviceName, "_", "-")
	k3dDNS := fmt.Sprintf("%s.%s.svc.cluster.local", normalized, ns)

	// If value already contains the normalized cluster-local form, leave it as-is.
	if strings.Contains(value, normalized+".svc.cluster.local") {
		return value
	}

	// Find either the normalized or original service name in the value and replace.
	idx := strings.Index(value, normalized)
	matchedName := normalized
	if idx < 0 {
		idx = strings.Index(value, serviceName)
		matchedName = serviceName
	}
	if idx < 0 {
		return value
	}

	prefix := value[:idx]
	rest := value[idx+len(matchedName):]
	if rest != "" && rest[0] == '.' {
		// Consume the old namespace/domain part up to the port.
		dotParts := strings.SplitN(rest[1:], ":", 2)
		if len(dotParts) == 2 {
			return prefix + k3dDNS + ":" + dotParts[1]
		}
		// No port — just domain.
		return prefix + k3dDNS
	}
	if rest != "" && rest[0] == ':' {
		// Direct service:port pattern.
		return prefix + k3dDNS + rest
	}

	return prefix + k3dDNS + rest
}

// isDSNEnvVar checks if an env var name is a known database DSN variable.
func isDSNEnvVar(name string) bool {
	return slices.Contains(dsnEnvVars, name)
}

// K3dServiceDNS returns the k3d internal DNS name for a service.
// Normalizes underscores to hyphens for DNS-safe labels.
func K3dServiceDNS(serviceName, namespace string, port int) string {
	if namespace == "" {
		namespace = defaultNamespace
	}
	normalized := strings.ReplaceAll(serviceName, "_", "-")
	return fmt.Sprintf("%s.%s.svc.cluster.local:%d", normalized, namespace, port)
}

func clonePorts(src map[string]int) map[string]int {
	if len(src) == 0 {
		return nil
	}
	dst := make(map[string]int, len(src))
	maps.Copy(dst, src)
	return dst
}

// kustomization is the kustomization.yaml structure we generate.
type kustomization struct {
	APIVersion string   `yaml:"apiVersion"`
	Kind       string   `yaml:"kind"`
	Namespace  string   `yaml:"namespace,omitempty"`
	Resources  []string `yaml:"resources,omitempty"`
	Patches    []patch  `yaml:"patches,omitempty"`
}

type patch struct {
	Target patchTarget `yaml:"target"`
	Patch  string      `yaml:"patch"`
}

type patchTarget struct {
	Kind string `yaml:"kind"`
	Name string `yaml:"name"`
}

// writeOverlay writes the overlay files for a single service.
func writeOverlay(dir string, overlay *ServiceOverlay) error {
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}

	// Build the strategic-merge patch that updates the workload image + env.
	workloadPatch := buildWorkloadPatchYAML(overlay)

	// Write kustomization.yaml.
	kust := kustomization{
		APIVersion: "kustomize.config.k8s.io/v1beta1",
		Kind:       "Kustomization",
		Namespace:  overlay.Namespace,
	}

	switch {
	case overlay.Synthetic:
		if err := os.WriteFile(filepath.Join(dir, "base.yaml"), []byte(buildSyntheticBaseYAML(overlay)), 0o600); err != nil {
			return fmt.Errorf("write base.yaml: %w", err)
		}
		kust.Resources = []string{"base.yaml"}
	case overlay.KustomizeBase != "":
		kust.Resources = []string{relativeBasePath(dir, overlay.KustomizeBase)}
	}

	if workloadPatch != "" {
		kust.Patches = []patch{
			{
				Target: patchTarget{
					Kind: overlay.WorkloadKind,
					Name: overlay.WorkloadName,
				},
				Patch: workloadPatch,
			},
		}
	}

	kustData, err := yaml.Marshal(kust)
	if err != nil {
		return fmt.Errorf("marshal kustomization: %w", err)
	}

	if err := os.WriteFile(filepath.Join(dir, "kustomization.yaml"), kustData, 0o600); err != nil {
		return fmt.Errorf("write kustomization.yaml: %w", err)
	}

	// Write env patches as a separate JSON file for reference/debugging.
	if len(overlay.EnvPatches) > 0 {
		patchData, marshalErr := json.MarshalIndent(overlay.EnvPatches, "", "  ")
		if marshalErr != nil {
			return fmt.Errorf("marshal env patches: %w", marshalErr)
		}
		if writeErr := os.WriteFile(filepath.Join(dir, "env-patches.json"), patchData, 0o600); writeErr != nil {
			return fmt.Errorf("write env-patches.json: %w", writeErr)
		}
	}

	return nil
}

func relativeBasePath(dir, base string) string {
	if base == "" || !filepath.IsAbs(base) {
		return base
	}
	absDir, err := filepath.Abs(dir)
	if err != nil {
		return base
	}
	rel, err := filepath.Rel(absDir, base)
	if err != nil {
		return base
	}
	return filepath.Clean(rel)
}

// buildWorkloadPatchYAML generates a strategic merge patch YAML string that
// updates the target workload's image and environment variables.
func buildWorkloadPatchYAML(overlay *ServiceOverlay) string {
	if len(overlay.EnvPatches) == 0 && overlay.Image == "" &&
		len(overlay.VolumeMounts) == 0 && len(overlay.Volumes) == 0 {
		return ""
	}

	// Sort env var names for deterministic output.
	names := make([]string, 0, len(overlay.EnvPatches))
	for k := range overlay.EnvPatches {
		names = append(names, k)
	}
	slices.Sort(names)

	var b strings.Builder
	b.WriteString("apiVersion: apps/v1\n")
	fmt.Fprintf(&b, "kind: %s\n", overlay.WorkloadKind)
	b.WriteString("metadata:\n")
	fmt.Fprintf(&b, "  name: %s\n", overlay.WorkloadName)
	fmt.Fprintf(&b, "  namespace: %s\n", overlay.Namespace)
	b.WriteString("spec:\n")
	b.WriteString("  template:\n")
	b.WriteString("    spec:\n")
	b.WriteString("      containers:\n")
	fmt.Fprintf(&b, "      - name: %s\n", overlay.ContainerName)
	if overlay.Image != "" {
		fmt.Fprintf(&b, "        image: %s\n", overlay.Image)
	}
	if len(names) > 0 {
		b.WriteString("        env:\n")
		for _, name := range names {
			fmt.Fprintf(&b, "        - name: %s\n", name)
			fmt.Fprintf(&b, "          value: %q\n", overlay.EnvPatches[name])
		}
	}
	if len(overlay.VolumeMounts) > 0 {
		b.WriteString("        volumeMounts:\n")
		for _, mount := range overlay.VolumeMounts {
			fmt.Fprintf(&b, "        - name: %s\n", mount.Name)
			fmt.Fprintf(&b, "          mountPath: %s\n", mount.MountPath)
		}
	}
	if len(overlay.Volumes) > 0 {
		b.WriteString("      volumes:\n")
		for _, volume := range overlay.Volumes {
			fmt.Fprintf(&b, "      - name: %s\n", volume.Name)
			b.WriteString("        hostPath:\n")
			fmt.Fprintf(&b, "          path: %s\n", volume.HostPath)
			b.WriteString("          type: DirectoryOrCreate\n")
		}
	}

	return b.String()
}

func buildSyntheticBaseYAML(overlay *ServiceOverlay) string {
	portNames := make([]string, 0, len(overlay.Ports))
	for name := range overlay.Ports {
		portNames = append(portNames, name)
	}
	slices.Sort(portNames)

	var b strings.Builder
	b.WriteString("apiVersion: v1\n")
	b.WriteString("kind: Service\n")
	b.WriteString("metadata:\n")
	fmt.Fprintf(&b, "  name: %s\n", overlay.Name)
	fmt.Fprintf(&b, "  namespace: %s\n", overlay.Namespace)
	b.WriteString("spec:\n")
	b.WriteString("  selector:\n")
	fmt.Fprintf(&b, "    app: %s\n", overlay.Name)
	b.WriteString("  ports:\n")
	for _, name := range portNames {
		fmt.Fprintf(&b, "  - name: %s\n", name)
		fmt.Fprintf(&b, "    port: %d\n", overlay.Ports[name])
		fmt.Fprintf(&b, "    targetPort: %d\n", overlay.Ports[name])
	}
	b.WriteString("---\n")
	b.WriteString("apiVersion: apps/v1\n")
	b.WriteString("kind: Deployment\n")
	b.WriteString("metadata:\n")
	fmt.Fprintf(&b, "  name: %s\n", overlay.Name)
	fmt.Fprintf(&b, "  namespace: %s\n", overlay.Namespace)
	b.WriteString("spec:\n")
	b.WriteString("  replicas: 1\n")
	b.WriteString("  selector:\n")
	b.WriteString("    matchLabels:\n")
	fmt.Fprintf(&b, "      app: %s\n", overlay.Name)
	b.WriteString("  template:\n")
	b.WriteString("    metadata:\n")
	b.WriteString("      labels:\n")
	fmt.Fprintf(&b, "        app: %s\n", overlay.Name)
	b.WriteString("    spec:\n")
	b.WriteString("      containers:\n")
	fmt.Fprintf(&b, "      - name: %s\n", overlay.ContainerName)
	fmt.Fprintf(&b, "        image: %s\n", overlay.Image)
	if len(portNames) > 0 {
		b.WriteString("        ports:\n")
		for _, name := range portNames {
			fmt.Fprintf(&b, "        - name: %s\n", name)
			fmt.Fprintf(&b, "          containerPort: %d\n", overlay.Ports[name])
		}
	}
	if len(overlay.EnvPatches) > 0 {
		envNames := make([]string, 0, len(overlay.EnvPatches))
		for name := range overlay.EnvPatches {
			envNames = append(envNames, name)
		}
		slices.Sort(envNames)
		b.WriteString("        env:\n")
		for _, name := range envNames {
			fmt.Fprintf(&b, "        - name: %s\n", name)
			fmt.Fprintf(&b, "          value: %q\n", overlay.EnvPatches[name])
		}
	}
	if httpPort, ok := overlay.Ports["http"]; ok {
		b.WriteString("        readinessProbe:\n")
		b.WriteString("          httpGet:\n")
		b.WriteString("            path: /healthz\n")
		fmt.Fprintf(&b, "            port: %d\n", httpPort)
		b.WriteString("          initialDelaySeconds: 2\n")
		b.WriteString("          periodSeconds: 2\n")
	}

	return b.String()
}
