// Controller: asigna IPs de los pools a los Services type=LoadBalancer.
package main

import (
	"context"
	"flag"
	"log"
	"net/netip"
	"os"
	"os/signal"
	"syscall"
	"time"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/informers"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"

	"github.com/jviejo/lb-k8s/internal/pool"
)

const (
	LoadBalancerClass = "lbk8s.io/l2"
	AnnPool           = "lbk8s.io/address-pool"
	AnnIP             = "lbk8s.io/loadBalancerIPs"
)

func main() {
	cfgPath := flag.String("config", "/etc/lbk8s/config.yaml", "fichero de pools")
	kubeconfig := flag.String("kubeconfig", "", "kubeconfig (vacío = in-cluster)")
	flag.Parse()

	cs := mustClient(*kubeconfig)
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	trigger := make(chan struct{}, 1)
	kick := func() {
		select {
		case trigger <- struct{}{}:
		default:
		}
	}
	factory := informers.NewSharedInformerFactory(cs, 0)
	svcInf := factory.Core().V1().Services()
	svcInf.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    func(any) { kick() },
		UpdateFunc: func(_, _ any) { kick() },
		DeleteFunc: func(any) { kick() },
	})
	factory.Start(ctx.Done())
	factory.WaitForCacheSync(ctx.Done())

	log.Println("controller iniciado")
	tick := time.NewTicker(30 * time.Second) // recoge cambios del ConfigMap
	defer tick.Stop()
	kick()
	for {
		select {
		case <-ctx.Done():
			return
		case <-trigger:
		case <-tick.C:
		}
		pools, err := loadPools(*cfgPath)
		if err != nil {
			log.Printf("ERROR config: %v", err)
			continue
		}
		svcs, _ := svcInf.Lister().List(everything{})
		reconcile(ctx, cs, pools, svcs)
	}
}

func loadPools(path string) ([]*pool.Pool, error) {
	b, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}
	return pool.Parse(b)
}

func ours(s *corev1.Service) bool {
	if s.Spec.Type != corev1.ServiceTypeLoadBalancer {
		return false
	}
	c := s.Spec.LoadBalancerClass
	return c == nil || *c == LoadBalancerClass
}

func currentIP(s *corev1.Service) (netip.Addr, bool) {
	for _, ing := range s.Status.LoadBalancer.Ingress {
		if ip, err := netip.ParseAddr(ing.IP); err == nil {
			return ip, true
		}
	}
	return netip.Addr{}, false
}

func reconcile(ctx context.Context, cs kubernetes.Interface, pools []*pool.Pool, svcs []*corev1.Service) {
	used := map[netip.Addr]bool{}
	var pending []*corev1.Service

	// 1) Registrar IPs en uso y liberar las de servicios que ya no son LB.
	for _, s := range svcs {
		ip, has := currentIP(s)
		switch {
		case !ours(s):
			if has && pool.Find(pools, ip) != nil {
				setIngress(ctx, cs, s, nil)
			}
		case has && pool.Find(pools, ip) != nil && !used[ip] && wantsSameIP(s, ip):
			used[ip] = true
		default:
			pending = append(pending, s)
		}
	}

	// 2) Asignar IPs a los pendientes.
	for _, s := range pending {
		ip, ok := allocate(s, pools, used)
		if !ok {
			if _, has := currentIP(s); has {
				setIngress(ctx, cs, s, nil)
			}
			log.Printf("sin IP disponible para %s/%s", s.Namespace, s.Name)
			continue
		}
		used[ip] = true
		if cur, has := currentIP(s); has && cur == ip {
			continue
		}
		log.Printf("asignando %s a %s/%s", ip, s.Namespace, s.Name)
		setIngress(ctx, cs, s, &ip)
	}
}

// wantsSameIP: si el usuario pide una IP concreta, la actual debe coincidir.
func wantsSameIP(s *corev1.Service, cur netip.Addr) bool {
	if want, ok := requestedIP(s); ok {
		return want == cur
	}
	if name := s.Annotations[AnnPool]; name != "" {
		return true // se valida al reasignar; simplificación
	}
	return true
}

func requestedIP(s *corev1.Service) (netip.Addr, bool) {
	v := s.Annotations[AnnIP]
	if v == "" {
		v = s.Spec.LoadBalancerIP
	}
	if v == "" {
		return netip.Addr{}, false
	}
	ip, err := netip.ParseAddr(v)
	return ip, err == nil
}

func allocate(s *corev1.Service, pools []*pool.Pool, used map[netip.Addr]bool) (netip.Addr, bool) {
	if ip, ok := requestedIP(s); ok {
		if pool.Find(pools, ip) != nil && !used[ip] {
			return ip, true
		}
		return netip.Addr{}, false
	}
	wanted := s.Annotations[AnnPool]
	for _, p := range pools {
		if wanted != "" && p.Name != wanted {
			continue
		}
		if wanted == "" && !p.AutoAssign {
			continue
		}
		if ip, ok := p.Next(used); ok {
			return ip, true
		}
	}
	return netip.Addr{}, false
}

func setIngress(ctx context.Context, cs kubernetes.Interface, s *corev1.Service, ip *netip.Addr) {
	n := s.DeepCopy()
	n.Status.LoadBalancer.Ingress = nil
	if ip != nil {
		n.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{{IP: ip.String()}}
	}
	if _, err := cs.CoreV1().Services(s.Namespace).UpdateStatus(ctx, n, metav1UpdateOpts); err != nil {
		log.Printf("ERROR actualizando %s/%s: %v", s.Namespace, s.Name, err)
	}
}

func mustClient(kubeconfig string) kubernetes.Interface {
	var cfg *rest.Config
	var err error
	if kubeconfig != "" {
		cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
	} else {
		cfg, err = rest.InClusterConfig()
	}
	if err != nil {
		log.Fatal(err)
	}
	return kubernetes.NewForConfigOrDie(cfg)
}
