// Package pool parsea la configuración de pools de IPs y asigna direcciones.
package pool

import (
	"fmt"
	"net/netip"
	"strings"

	"sigs.k8s.io/yaml"
)

// Config es el contenido del fichero de configuración.
type Config struct {
	Pools []PoolSpec `json:"pools"`
}

type PoolSpec struct {
	Name       string   `json:"name"`
	Addresses  []string `json:"addresses"`            // "a.b.c.d-e.f.g.h" o "a.b.c.d/nn"
	AutoAssign *bool    `json:"autoAssign,omitempty"` // por defecto true
}

type Pool struct {
	Name       string
	AutoAssign bool
	ranges     [][2]netip.Addr
}

func Parse(data []byte) ([]*Pool, error) {
	var cfg Config
	if err := yaml.UnmarshalStrict(data, &cfg); err != nil {
		return nil, fmt.Errorf("config inválida: %w", err)
	}
	var pools []*Pool
	seen := map[string]bool{}
	for _, ps := range cfg.Pools {
		if ps.Name == "" || seen[ps.Name] {
			return nil, fmt.Errorf("pool sin nombre o duplicado: %q", ps.Name)
		}
		seen[ps.Name] = true
		p := &Pool{Name: ps.Name, AutoAssign: ps.AutoAssign == nil || *ps.AutoAssign}
		for _, a := range ps.Addresses {
			r, err := parseRange(a)
			if err != nil {
				return nil, fmt.Errorf("pool %s: %w", ps.Name, err)
			}
			p.ranges = append(p.ranges, r)
		}
		pools = append(pools, p)
	}
	return pools, nil
}

func parseRange(s string) ([2]netip.Addr, error) {
	s = strings.TrimSpace(s)
	if strings.Contains(s, "/") {
		pfx, err := netip.ParsePrefix(s)
		if err != nil || !pfx.Addr().Is4() {
			return [2]netip.Addr{}, fmt.Errorf("CIDR IPv4 inválido %q", s)
		}
		pfx = pfx.Masked()
		return [2]netip.Addr{pfx.Addr(), lastOfPrefix(pfx)}, nil
	}
	parts := strings.Split(s, "-")
	if len(parts) != 2 {
		return [2]netip.Addr{}, fmt.Errorf("rango inválido %q (usa a-b o CIDR)", s)
	}
	a, err1 := netip.ParseAddr(strings.TrimSpace(parts[0]))
	b, err2 := netip.ParseAddr(strings.TrimSpace(parts[1]))
	if err1 != nil || err2 != nil || !a.Is4() || !b.Is4() || b.Less(a) {
		return [2]netip.Addr{}, fmt.Errorf("rango IPv4 inválido %q", s)
	}
	return [2]netip.Addr{a, b}, nil
}

func lastOfPrefix(p netip.Prefix) netip.Addr {
	b := p.Addr().As4()
	host := 32 - p.Bits()
	v := uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
	if host > 0 {
		v |= (1 << uint(host)) - 1
	}
	return netip.AddrFrom4([4]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}

// Contains indica si la IP pertenece al pool.
func (p *Pool) Contains(ip netip.Addr) bool {
	for _, r := range p.ranges {
		if !ip.Less(r[0]) && !r[1].Less(ip) {
			return true
		}
	}
	return false
}

// Next devuelve la primera IP libre del pool.
func (p *Pool) Next(used map[netip.Addr]bool) (netip.Addr, bool) {
	for _, r := range p.ranges {
		for ip := r[0]; ; ip = ip.Next() {
			if !used[ip] {
				return ip, true
			}
			if ip == r[1] {
				break
			}
		}
	}
	return netip.Addr{}, false
}

// Find devuelve el pool que contiene la IP.
func Find(pools []*Pool, ip netip.Addr) *Pool {
	for _, p := range pools {
		if p.Contains(ip) {
			return p
		}
	}
	return nil
}
