package deployer

import (
	"context"
	"fmt"
	"slices"
	"strings"

	networkingv1 "k8s.io/api/networking/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// AddIngressPath adds or updates a path rule in the project's unified Ingress.
// For monorepo projects, all components share a single Ingress with path-based routing.
// Paths are ordered by specificity (most specific first) to ensure correct matching.
// Uses retry with exponential backoff on K8s conflict errors.
func (d *Deployer) AddIngressPath(ctx context.Context, projectName, host, path, serviceName string, servicePort int) error {
	ns := d.config.Namespace

	return retryOnConflict(ctx, func() error {
		// Get existing ingress or create a new one
		ingress, err := d.ingressClient.GetIngress(ctx, ns, projectName)
		if errors.IsNotFound(err) {
			// Create new ingress with this path
			return d.createUnifiedIngress(ctx, projectName, host, path, serviceName, servicePort)
		}
		if err != nil {
			return fmt.Errorf("failed to get ingress: %w", err)
		}

		// Find the rule for this host
		var ruleIndex = -1
		for i, rule := range ingress.Spec.Rules {
			if rule.Host == host {
				ruleIndex = i
				break
			}
		}

		pathType := networkingv1.PathTypePrefix
		newPath := networkingv1.HTTPIngressPath{
			Path:     path,
			PathType: &pathType,
			Backend: networkingv1.IngressBackend{
				Service: &networkingv1.IngressServiceBackend{
					Name: serviceName,
					Port: networkingv1.ServiceBackendPort{
						Number: int32(servicePort),
					},
				},
			},
		}

		if ruleIndex >= 0 {
			// Update existing rule - add or replace path
			paths := ingress.Spec.Rules[ruleIndex].HTTP.Paths
			updated := false
			for i, p := range paths {
				if p.Path == path {
					paths[i] = newPath
					updated = true
					break
				}
			}
			if !updated {
				paths = append(paths, newPath)
			}
			// Sort paths by specificity (longer paths first)
			sortIngressPaths(paths)
			ingress.Spec.Rules[ruleIndex].HTTP.Paths = paths
		} else {
			// Add new rule for this host
			ingress.Spec.Rules = append(ingress.Spec.Rules, networkingv1.IngressRule{
				Host: host,
				IngressRuleValue: networkingv1.IngressRuleValue{
					HTTP: &networkingv1.HTTPIngressRuleValue{
						Paths: []networkingv1.HTTPIngressPath{newPath},
					},
				},
			})

			// Add TLS entry for the new host if not already present
			hostHasTLS := false
			for _, tls := range ingress.Spec.TLS {
				if slices.Contains(tls.Hosts, host) {
					hostHasTLS = true
					break
				}
			}
			if !hostHasTLS {
				tlsSecretName := strings.ReplaceAll(host, ".", "-") + "-tls"
				ingress.Spec.TLS = append(ingress.Spec.TLS, networkingv1.IngressTLS{
					Hosts:      []string{host},
					SecretName: tlsSecretName,
				})
			}
		}

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

		return nil
	})
}

// createUnifiedIngress creates a new project-level Ingress with a single path.
func (d *Deployer) createUnifiedIngress(ctx context.Context, projectName, host, path, serviceName string, servicePort int) error {
	ns := d.config.Namespace
	pathType := networkingv1.PathTypePrefix
	ingressClass := d.config.IngressClass
	tlsSecretName := strings.ReplaceAll(host, ".", "-") + "-tls"

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

	ingress := &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:        projectName,
			Namespace:   ns,
			Labels:      map[string]string{"project": projectName},
			Annotations: annotations,
		},
		Spec: networkingv1.IngressSpec{
			IngressClassName: &ingressClass,
			TLS: []networkingv1.IngressTLS{
				{
					Hosts:      []string{host},
					SecretName: tlsSecretName,
				},
			},
			Rules: []networkingv1.IngressRule{
				{
					Host: host,
					IngressRuleValue: networkingv1.IngressRuleValue{
						HTTP: &networkingv1.HTTPIngressRuleValue{
							Paths: []networkingv1.HTTPIngressPath{
								{
									Path:     path,
									PathType: &pathType,
									Backend: networkingv1.IngressBackend{
										Service: &networkingv1.IngressServiceBackend{
											Name: serviceName,
											Port: networkingv1.ServiceBackendPort{
												Number: int32(servicePort),
											},
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	_, err := d.ingressClient.CreateIngress(ctx, ns, ingress)
	if err != nil {
		return fmt.Errorf("failed to create ingress: %w", err)
	}

	return nil
}

// RemoveIngressPath removes a path rule from the project's unified Ingress.
// If no paths remain for a host, the host rule is removed.
// If no rules remain, the Ingress is deleted.
// Uses retry with exponential backoff on K8s conflict errors.
func (d *Deployer) RemoveIngressPath(ctx context.Context, projectName, host, path string) error {
	ns := d.config.Namespace

	return retryOnConflict(ctx, func() error {
		ingress, err := d.ingressClient.GetIngress(ctx, ns, projectName)
		if errors.IsNotFound(err) {
			return nil // Already gone
		}
		if err != nil {
			return fmt.Errorf("failed to get ingress: %w", err)
		}

		// Find and update the rule for this host
		var newRules []networkingv1.IngressRule
		for _, rule := range ingress.Spec.Rules {
			if rule.Host == host {
				// Filter out the path to remove
				var newPaths []networkingv1.HTTPIngressPath
				for _, p := range rule.HTTP.Paths {
					if p.Path != path {
						newPaths = append(newPaths, p)
					}
				}
				// Keep the rule only if it still has paths
				if len(newPaths) > 0 {
					rule.HTTP.Paths = newPaths
					newRules = append(newRules, rule)
				}
				// If no paths left, skip adding this rule (effectively removing it)
			} else {
				newRules = append(newRules, rule)
			}
		}

		// If no rules left, delete the ingress
		if len(newRules) == 0 {
			err = d.ingressClient.DeleteIngress(ctx, ns, projectName)
			if err != nil && !errors.IsNotFound(err) {
				return fmt.Errorf("failed to delete ingress: %w", err)
			}
			return nil
		}

		// Update TLS to remove hosts that no longer have rules
		activeHosts := make(map[string]bool)
		for _, rule := range newRules {
			activeHosts[rule.Host] = true
		}

		var newTLS []networkingv1.IngressTLS
		for _, tls := range ingress.Spec.TLS {
			var activeHostsInTLS []string
			for _, h := range tls.Hosts {
				if activeHosts[h] {
					activeHostsInTLS = append(activeHostsInTLS, h)
				}
			}
			if len(activeHostsInTLS) > 0 {
				tls.Hosts = activeHostsInTLS
				newTLS = append(newTLS, tls)
			}
		}

		ingress.Spec.Rules = newRules
		ingress.Spec.TLS = newTLS

		_, err = d.ingressClient.UpdateIngress(ctx, ns, ingress)
		if err != nil {
			return fmt.Errorf("failed to update ingress: %w", err)
		}

		return nil
	})
}

// sortIngressPaths sorts paths by specificity (longer paths first).
// This ensures more specific paths like /api/auth match before /api.
func sortIngressPaths(paths []networkingv1.HTTPIngressPath) {
	slices.SortFunc(paths, func(a, b networkingv1.HTTPIngressPath) int {
		// Longer paths first (descending length)
		if len(a.Path) != len(b.Path) {
			return len(b.Path) - len(a.Path)
		}
		// Same length: alphabetical for consistency
		return strings.Compare(a.Path, b.Path)
	})
}
