package deployer

import (
	"context"
	"fmt"
	"strings"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"

	"github.com/orchard9/rdev/internal/domain"
)

const (
	maxIngressRetries = 3
	retryBaseDelay    = 100 * time.Millisecond
)

// retryOnConflict executes fn with retry logic for K8s conflict errors.
// Uses exponential backoff: 100ms, 200ms, 400ms.
func retryOnConflict(ctx context.Context, fn func() error) error {
	var lastErr error
	for attempt := 0; attempt < maxIngressRetries; attempt++ {
		err := fn()
		if err == nil {
			return nil
		}
		lastErr = err

		// Only retry on conflict errors (optimistic locking failure)
		if !errors.IsConflict(err) {
			return err
		}

		// Check context before sleeping
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(retryBaseDelay * time.Duration(1<<attempt)):
		}
	}
	return fmt.Errorf("failed after %d retries: %w", maxIngressRetries, lastErr)
}

// sanitizeLabelValue converts a component path to a valid K8s label value.
// K8s labels must be alphanumeric with '-', '_', or '.' and must start/end with alphanumeric.
// Example: "services/api" -> "services-api"
func sanitizeLabelValue(path string) string {
	return strings.ReplaceAll(path, "/", "-")
}

// ensureNamespace verifies the deployment namespace exists, creating it only if needed.
func (d *Deployer) ensureNamespace(ctx context.Context) error {
	_, err := d.client.CoreV1().Namespaces().Get(ctx, d.config.Namespace, metav1.GetOptions{})
	if err == nil {
		return nil // namespace exists
	}
	if !errors.IsNotFound(err) {
		return err
	}

	// Namespace doesn't exist, try to create it
	ns := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: d.config.Namespace,
		},
	}
	_, err = d.client.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
	if err != nil && !errors.IsAlreadyExists(err) {
		return err
	}
	return nil
}

// createOrUpdateSecret manages the secret for environment variables.
func (d *Deployer) createOrUpdateSecret(ctx context.Context, spec domain.DeploySpec) error {
	deploymentName := spec.DeploymentName()
	secretName := deploymentName + "-env"
	ns := d.config.Namespace

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      secretName,
			Namespace: ns,
			Labels: map[string]string{
				"app":     deploymentName,
				"project": spec.ProjectName,
			},
		},
		StringData: spec.Secrets,
	}

	_, err := d.client.CoreV1().Secrets(ns).Get(ctx, secretName, metav1.GetOptions{})
	if errors.IsNotFound(err) {
		_, err = d.client.CoreV1().Secrets(ns).Create(ctx, secret, metav1.CreateOptions{})
	} else if err == nil {
		_, err = d.client.CoreV1().Secrets(ns).Update(ctx, secret, metav1.UpdateOptions{})
	}
	return err
}

// createOrUpdateDeployment manages the Kubernetes Deployment resource.
func (d *Deployer) createOrUpdateDeployment(ctx context.Context, spec domain.DeploySpec) error {
	ns := d.config.Namespace
	replicas := int32(spec.Replicas)
	deploymentName := spec.DeploymentName()

	// Build env vars
	var envVars []corev1.EnvVar
	for k, v := range spec.EnvVars {
		envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
	}

	// Inject sibling service URLs for service discovery
	for k, v := range spec.SiblingServices {
		envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
	}

	// Add secret env vars
	var envFrom []corev1.EnvFromSource
	if len(spec.Secrets) > 0 {
		envFrom = append(envFrom, corev1.EnvFromSource{
			SecretRef: &corev1.SecretEnvSource{
				LocalObjectReference: corev1.LocalObjectReference{
					Name: deploymentName + "-env",
				},
			},
		})
	}

	deployment := d.buildDeployment(spec, ns, replicas, envVars, envFrom)

	_, err := d.client.AppsV1().Deployments(ns).Get(ctx, deploymentName, metav1.GetOptions{})
	if errors.IsNotFound(err) {
		_, err = d.client.AppsV1().Deployments(ns).Create(ctx, deployment, metav1.CreateOptions{})
	} else if err == nil {
		_, err = d.client.AppsV1().Deployments(ns).Update(ctx, deployment, metav1.UpdateOptions{})
	}
	return err
}

func (d *Deployer) buildDeployment(spec domain.DeploySpec, ns string, replicas int32, envVars []corev1.EnvVar, envFrom []corev1.EnvFromSource) *appsv1.Deployment {
	deploymentName := spec.DeploymentName()

	// Build labels - always include project, component if present
	labels := map[string]string{
		"app":     deploymentName,
		"project": spec.ProjectName,
	}
	if spec.ComponentPath != "" {
		labels["component"] = sanitizeLabelValue(spec.ComponentPath)
	}

	return &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      deploymentName,
			Namespace: ns,
			Labels:    labels,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": deploymentName,
				},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: labels,
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:    deploymentName,
							Image:   spec.Image,
							Env:     envVars,
							EnvFrom: envFrom,
							Ports: []corev1.ContainerPort{
								{
									ContainerPort: int32(spec.Port),
									Protocol:      corev1.ProtocolTCP,
								},
							},
							Resources: corev1.ResourceRequirements{
								Requests: corev1.ResourceList{
									corev1.ResourceCPU:    resourceQuantity("100m"),
									corev1.ResourceMemory: resourceQuantity("128Mi"),
								},
								Limits: corev1.ResourceList{
									corev1.ResourceCPU:    resourceQuantity("1000m"),
									corev1.ResourceMemory: resourceQuantity("512Mi"),
								},
							},
						},
					},
				},
			},
		},
	}
}

// createOrUpdateService manages the Kubernetes Service resource.
func (d *Deployer) createOrUpdateService(ctx context.Context, spec domain.DeploySpec) error {
	ns := d.config.Namespace
	deploymentName := spec.DeploymentName()

	// Build labels
	labels := map[string]string{
		"app":     deploymentName,
		"project": spec.ProjectName,
	}
	if spec.ComponentPath != "" {
		labels["component"] = sanitizeLabelValue(spec.ComponentPath)
	}

	service := &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      deploymentName,
			Namespace: ns,
			Labels:    labels,
		},
		Spec: corev1.ServiceSpec{
			Selector: map[string]string{
				"app": deploymentName,
			},
			Ports: []corev1.ServicePort{
				{
					Port:       int32(spec.Port),
					TargetPort: intstr.FromInt(spec.Port),
					Protocol:   corev1.ProtocolTCP,
				},
			},
		},
	}

	_, err := d.client.CoreV1().Services(ns).Get(ctx, deploymentName, metav1.GetOptions{})
	if errors.IsNotFound(err) {
		_, err = d.client.CoreV1().Services(ns).Create(ctx, service, metav1.CreateOptions{})
	} else if err == nil {
		_, err = d.client.CoreV1().Services(ns).Update(ctx, service, metav1.UpdateOptions{})
	}
	return err
}

// createOrUpdateIngress manages the Kubernetes Ingress resource.
func (d *Deployer) createOrUpdateIngress(ctx context.Context, spec domain.DeploySpec) error {
	ns := d.config.Namespace
	pathType := networkingv1.PathTypePrefix
	ingressClass := d.config.IngressClass
	deploymentName := spec.DeploymentName()

	// Build TLS secret name from domain
	tlsSecretName := strings.ReplaceAll(spec.Domain, ".", "-") + "-tls"

	annotations := map[string]string{}
	if d.config.TLSIssuer != "" {
		annotations["cert-manager.io/cluster-issuer"] = d.config.TLSIssuer
	}

	ingress := d.buildIngress(spec, ns, pathType, ingressClass, tlsSecretName, annotations)

	_, err := d.client.NetworkingV1().Ingresses(ns).Get(ctx, deploymentName, metav1.GetOptions{})
	if errors.IsNotFound(err) {
		_, err = d.client.NetworkingV1().Ingresses(ns).Create(ctx, ingress, metav1.CreateOptions{})
	} else if err == nil {
		_, err = d.client.NetworkingV1().Ingresses(ns).Update(ctx, ingress, metav1.UpdateOptions{})
	}
	return err
}

func (d *Deployer) buildIngress(spec domain.DeploySpec, ns string, pathType networkingv1.PathType, ingressClass, tlsSecretName string, annotations map[string]string) *networkingv1.Ingress {
	deploymentName := spec.DeploymentName()

	// Build labels
	labels := map[string]string{
		"app":     deploymentName,
		"project": spec.ProjectName,
	}
	if spec.ComponentPath != "" {
		labels["component"] = sanitizeLabelValue(spec.ComponentPath)
	}

	// Use BasePath if specified, otherwise default to "/"
	path := "/"
	if spec.BasePath != "" {
		path = spec.BasePath
	}

	return &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:        deploymentName,
			Namespace:   ns,
			Labels:      labels,
			Annotations: annotations,
		},
		Spec: networkingv1.IngressSpec{
			IngressClassName: &ingressClass,
			TLS: []networkingv1.IngressTLS{
				{
					Hosts:      []string{spec.Domain},
					SecretName: tlsSecretName,
				},
			},
			Rules: []networkingv1.IngressRule{
				{
					Host: spec.Domain,
					IngressRuleValue: networkingv1.IngressRuleValue{
						HTTP: &networkingv1.HTTPIngressRuleValue{
							Paths: []networkingv1.HTTPIngressPath{
								{
									Path:     path,
									PathType: &pathType,
									Backend: networkingv1.IngressBackend{
										Service: &networkingv1.IngressServiceBackend{
											Name: deploymentName,
											Port: networkingv1.ServiceBackendPort{
												Number: int32(spec.Port),
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}
}

// resourceQuantity parses a resource quantity string.
// Returns the parsed quantity or a zero quantity on error.
func resourceQuantity(s string) resource.Quantity {
	q, _ := resource.ParseQuantity(s)
	return q
}

// AddIngressHost adds a new host to an existing project's ingress.
// This is used when adding domain aliases to a project.
// The host is added to both the TLS configuration and the routing rules.
func (d *Deployer) AddIngressHost(ctx context.Context, projectName, host string) error {
	ns := d.config.Namespace

	// Get existing ingress
	ingress, err := d.client.NetworkingV1().Ingresses(ns).Get(ctx, projectName, metav1.GetOptions{})
	if err != nil {
		return fmt.Errorf("failed to get ingress: %w", err)
	}

	// Check if host already exists
	for _, rule := range ingress.Spec.Rules {
		if rule.Host == host {
			return nil // Host already exists, nothing to do
		}
	}

	// Get the service port from existing rules (assumes all rules use same backend)
	var servicePort int32 = 80
	if len(ingress.Spec.Rules) > 0 && ingress.Spec.Rules[0].HTTP != nil && len(ingress.Spec.Rules[0].HTTP.Paths) > 0 {
		servicePort = ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number
	}

	// Add TLS entry for the new host
	tlsSecretName := strings.ReplaceAll(host, ".", "-") + "-tls"
	ingress.Spec.TLS = append(ingress.Spec.TLS, networkingv1.IngressTLS{
		Hosts:      []string{host},
		SecretName: tlsSecretName,
	})

	// Add routing rule for the new host
	pathType := networkingv1.PathTypePrefix
	ingress.Spec.Rules = append(ingress.Spec.Rules, networkingv1.IngressRule{
		Host: host,
		IngressRuleValue: networkingv1.IngressRuleValue{
			HTTP: &networkingv1.HTTPIngressRuleValue{
				Paths: []networkingv1.HTTPIngressPath{
					{
						Path:     "/",
						PathType: &pathType,
						Backend: networkingv1.IngressBackend{
							Service: &networkingv1.IngressServiceBackend{
								Name: projectName,
								Port: networkingv1.ServiceBackendPort{
									Number: servicePort,
								},
							},
						},
					},
				},
			},
		},
	})

	// Update the ingress
	_, err = d.client.NetworkingV1().Ingresses(ns).Update(ctx, ingress, metav1.UpdateOptions{})
	if err != nil {
		return fmt.Errorf("failed to update ingress: %w", err)
	}

	return nil
}

// RemoveIngressHost removes a host from an existing project's ingress.
// This is used when removing domain aliases from a project.
func (d *Deployer) RemoveIngressHost(ctx context.Context, projectName, host string) error {
	ns := d.config.Namespace

	// Get existing ingress
	ingress, err := d.client.NetworkingV1().Ingresses(ns).Get(ctx, projectName, metav1.GetOptions{})
	if err != nil {
		return fmt.Errorf("failed to get ingress: %w", err)
	}

	// Remove from TLS entries
	var newTLS []networkingv1.IngressTLS
	for _, tls := range ingress.Spec.TLS {
		// Keep TLS entries that don't contain this host
		var newHosts []string
		for _, h := range tls.Hosts {
			if h != host {
				newHosts = append(newHosts, h)
			}
		}
		if len(newHosts) > 0 {
			tls.Hosts = newHosts
			newTLS = append(newTLS, tls)
		}
	}
	ingress.Spec.TLS = newTLS

	// Remove from routing rules
	var newRules []networkingv1.IngressRule
	for _, rule := range ingress.Spec.Rules {
		if rule.Host != host {
			newRules = append(newRules, rule)
		}
	}
	ingress.Spec.Rules = newRules

	// Update the ingress
	_, err = d.client.NetworkingV1().Ingresses(ns).Update(ctx, ingress, metav1.UpdateOptions{})
	if err != nil {
		return fmt.Errorf("failed to update ingress: %w", err)
	}

	return nil
}
