package kustomize

import (
	"errors"
	"fmt"
	"maps"
	"os"
	"path/filepath"
	"strings"

	"gopkg.in/yaml.v3"
)

// errNotApplication is returned when a YAML file is not an ArgoCD Application.
var errNotApplication = errors.New("not an ArgoCD Application")

// argoApp is a minimal representation of an ArgoCD Application manifest.
// We only parse the fields needed for overlay generation.
type argoApp struct {
	APIVersion string      `yaml:"apiVersion"`
	Kind       string      `yaml:"kind"`
	Metadata   argoMeta    `yaml:"metadata"`
	Spec       argoAppSpec `yaml:"spec"`
}

type argoMeta struct {
	Name      string `yaml:"name"`
	Namespace string `yaml:"namespace"`
}

type argoAppSpec struct {
	Source      argoSource      `yaml:"source"`
	Destination argoDestination `yaml:"destination"`
}

type argoSource struct {
	RepoURL        string        `yaml:"repoURL"`
	Path           string        `yaml:"path"`
	TargetRevision string        `yaml:"targetRevision"`
	Kustomize      argoKustomize `yaml:"kustomize,omitempty"`
}

type argoKustomize struct {
	Images []string `yaml:"images,omitempty"`
}

type argoDestination struct {
	Namespace string `yaml:"namespace"`
}

// k8sWorkload is a minimal representation for parsing Deployment / StatefulSet
// manifests found in the kustomize base paths.
type k8sWorkload struct {
	APIVersion string     `yaml:"apiVersion"`
	Kind       string     `yaml:"kind"`
	Metadata   argoMeta   `yaml:"metadata"`
	Spec       deploySpec `yaml:"spec"`
}

type deploySpec struct {
	Template podTemplate `yaml:"template"`
}

type podTemplate struct {
	Spec podSpec `yaml:"spec"`
}

type podSpec struct {
	Containers []container `yaml:"containers"`
}

type container struct {
	Name  string          `yaml:"name"`
	Image string          `yaml:"image"`
	Env   []envVar        `yaml:"env,omitempty"`
	Ports []containerPort `yaml:"ports,omitempty"`
}

type envVar struct {
	Name  string `yaml:"name"`
	Value string `yaml:"value,omitempty"`
}

type containerPort struct {
	Name          string `yaml:"name,omitempty"`
	ContainerPort int    `yaml:"containerPort"`
	Protocol      string `yaml:"protocol,omitempty"`
}

type workloadSpec struct {
	Kind          string
	Name          string
	ContainerName string
	Image         string
	Env           map[string]string
	Ports         map[string]int
}

// ParseStagingConfigs reads ArgoCD Application manifests from the app-delivery
// repo and extracts service configuration needed for overlay generation.
//
// It looks for YAML files in:
//   - <appDeliveryPath>/app-registry/
//   - <appDeliveryPath>/staging/app-registry/
//
// For each Application, it also attempts to read the referenced kustomize base
// to extract Deployment env vars and ports for endpoint rewriting.
//
// Returns a map of service name → StagingConfig.
func ParseStagingConfigs(appDeliveryPath string) (map[string]*StagingConfig, error) {
	if appDeliveryPath == "" {
		return nil, fmt.Errorf("app_delivery path is empty")
	}

	configs := make(map[string]*StagingConfig)

	// Try both common ArgoCD app registry locations.
	searchDirs := []string{
		filepath.Join(appDeliveryPath, "app-registry"),
		filepath.Join(appDeliveryPath, "staging", "app-registry"),
	}

	for _, dir := range searchDirs {
		entries, err := os.ReadDir(dir)
		if err != nil {
			if os.IsNotExist(err) {
				continue
			}
			return nil, fmt.Errorf("read app-registry dir %s: %w", dir, err)
		}

		for _, entry := range entries {
			if entry.IsDir() || !isYAML(entry.Name()) {
				continue
			}

			path := filepath.Join(dir, entry.Name())
			sc, err := parseStagingFile(path)
			if err != nil {
				// Skip files that are not ArgoCD Application manifests, but surface
				// unexpected read/parse errors with file context.
				if errors.Is(err, errNotApplication) {
					continue
				}
				return nil, fmt.Errorf("parse staging file %s: %w", path, err)
			}
			configs[sc.Name] = sc
		}
	}

	// Populate Env/Ports by reading Deployment manifests from kustomize bases.
	for _, sc := range configs {
		if sc.KustomizePath == "" {
			continue
		}
		populateEnvPorts(appDeliveryPath, sc)
	}

	return configs, nil
}

// populateEnvPorts reads workload manifests from the kustomize base path and
// fills in the StagingConfig's rollout metadata plus env / ports.
func populateEnvPorts(appDeliveryPath string, sc *StagingConfig) {
	baseDir := filepath.Join(appDeliveryPath, sc.KustomizePath)
	entries, err := os.ReadDir(baseDir)
	if err != nil {
		return // base path may not exist locally
	}

	for _, entry := range entries {
		if entry.IsDir() || !isYAML(entry.Name()) {
			continue
		}
		path := filepath.Join(baseDir, entry.Name())
		spec, parseErr := parseWorkloadSpec(path)
		if parseErr != nil {
			continue
		}
		if sc.WorkloadKind == "" {
			sc.WorkloadKind = spec.Kind
		}
		if sc.WorkloadName == "" {
			sc.WorkloadName = spec.Name
		}
		if sc.ContainerName == "" {
			sc.ContainerName = spec.ContainerName
		}
		if sc.Image == "" {
			sc.Image = spec.Image
		}
		// Merge into staging config.
		maps.Copy(sc.Env, spec.Env)
		maps.Copy(sc.Ports, spec.Ports)
	}
}

// parseStagingFile parses a single ArgoCD Application YAML file.
// Only Application kind is supported; ApplicationSet has a different structure.
func parseStagingFile(path string) (*StagingConfig, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	var app argoApp
	if err := yaml.Unmarshal(data, &app); err != nil {
		return nil, err
	}

	// Only process ArgoCD Application manifests.
	// ApplicationSet has a different spec structure and is not supported.
	if app.Kind != "Application" {
		return nil, errNotApplication
	}

	name := app.Metadata.Name
	if name == "" {
		return nil, errNotApplication
	}

	sc := &StagingConfig{
		Name:          name,
		Namespace:     app.Spec.Destination.Namespace,
		KustomizePath: app.Spec.Source.Path,
		Env:           make(map[string]string),
		Ports:         make(map[string]int),
	}

	// Do not fall back to metadata.namespace — for ArgoCD Applications this is
	// typically the ArgoCD control-plane namespace (e.g. "argocd"), not the
	// target workload namespace. Leave empty and let downstream default it.

	// Extract image from kustomize images if available.
	if len(app.Spec.Source.Kustomize.Images) > 0 {
		sc.Image = app.Spec.Source.Kustomize.Images[0]
	}

	return sc, nil
}

// parseWorkloadSpec reads a Kubernetes Deployment / StatefulSet YAML and extracts
// image, environment variables, and ports from the first container.
func parseWorkloadSpec(path string) (*workloadSpec, error) {
	data, readErr := os.ReadFile(path)
	if readErr != nil {
		return nil, readErr
	}

	var workload k8sWorkload
	if unmarshalErr := yaml.Unmarshal(data, &workload); unmarshalErr != nil {
		return nil, unmarshalErr
	}

	switch workload.Kind {
	case "Deployment", "StatefulSet":
	default:
		return nil, fmt.Errorf("expected kind Deployment or StatefulSet, got %q", workload.Kind)
	}

	spec := &workloadSpec{
		Kind:  workload.Kind,
		Name:  workload.Metadata.Name,
		Env:   make(map[string]string),
		Ports: make(map[string]int),
	}

	if len(workload.Spec.Template.Spec.Containers) == 0 {
		return spec, nil
	}

	c := workload.Spec.Template.Spec.Containers[0]
	spec.ContainerName = c.Name
	spec.Image = c.Image
	for _, e := range c.Env {
		if e.Value != "" {
			spec.Env[e.Name] = e.Value
		}
	}
	for _, p := range c.Ports {
		name := p.Name
		if name == "" {
			name = fmt.Sprintf("port-%d", p.ContainerPort)
		}
		spec.Ports[name] = p.ContainerPort
	}

	return spec, nil
}

// ParseDeploymentEnvPorts reads a Kubernetes Deployment YAML and extracts
// environment variables and ports from the first container.
// Returns an error if the YAML is not a Deployment.
func ParseDeploymentEnvPorts(path string) (env map[string]string, ports map[string]int, err error) {
	spec, err := parseWorkloadSpec(path)
	if err != nil {
		return nil, nil, err
	}
	if spec.Kind != "Deployment" {
		return nil, nil, fmt.Errorf("expected kind Deployment, got %q", spec.Kind)
	}
	return spec.Env, spec.Ports, nil
}

func isYAML(name string) bool {
	return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml")
}
