"""Subnet and routing table allocator for chain networking.

Manages the 10.200.0.0/16 pool, allocating /30 subnets for private veth pairs.
Each allocation also gets a routing table ID and ip rule priority.

State is persisted to ~/.config/gaol/subnet-allocations.yaml so allocations
survive reboots and are consistent across chain start/stop cycles.
"""

from __future__ import annotations

import ipaddress
from pathlib import Path

import yaml
from pydantic import BaseModel, Field

from gaol.networking.archetypes import VethConfig


class SubnetAllocation(BaseModel):
    """A single allocated /30 subnet."""

    container_name: str
    subnet: str  # "10.200.1.0/30"
    host_ip: str  # "10.200.1.1"
    container_ip: str  # "10.200.1.2"
    host_iface: str  # "ve-research"
    table_id: int  # routing table number
    priority: int  # ip rule priority


class AllocationState(BaseModel):
    """Persisted allocation state."""

    allocations: list[SubnetAllocation] = Field(default_factory=list)
    next_octet: int = 1  # next third-octet to try (1-255)
    next_table_id: int = 200
    next_priority: int = 100


# The pool: 10.200.0.0/16, carved into /30 subnets (4 IPs each).
# We use the third octet as the subnet index: 10.200.{1..255}.0/30
# This gives 255 subnets, which is more than enough.
_POOL_BASE = ipaddress.IPv4Network("10.200.0.0/16")
_STATE_FILE = "subnet-allocations.yaml"


def _get_state_path() -> Path:
    """Get the path to the allocation state file."""
    config_dir = Path.home() / ".config" / "gaol"
    config_dir.mkdir(parents=True, exist_ok=True)
    return config_dir / _STATE_FILE


def _load_state(path: Path | None = None) -> AllocationState:
    """Load allocation state from disk."""
    state_path = path or _get_state_path()
    if not state_path.exists():
        return AllocationState()
    data = yaml.safe_load(state_path.read_text())
    if not data:
        return AllocationState()
    return AllocationState.model_validate(data)


def _save_state(state: AllocationState, path: Path | None = None) -> None:
    """Save allocation state to disk."""
    state_path = path or _get_state_path()
    state_path.parent.mkdir(parents=True, exist_ok=True)
    data = state.model_dump(mode="json")
    state_path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False))


def _used_octets(state: AllocationState) -> set[int]:
    """Get the set of third-octets already allocated."""
    octets: set[int] = set()
    for alloc in state.allocations:
        net = ipaddress.IPv4Network(alloc.subnet)
        octets.add(int(net.network_address) >> 8 & 0xFF)
    return octets


def allocate(
    container_name: str,
    host_iface: str | None = None,
    state_path: Path | None = None,
) -> SubnetAllocation:
    """Allocate a /30 subnet, routing table, and priority for a container.

    If the container already has an allocation, returns the existing one.

    Args:
        container_name: Container name (used as key).
        host_iface: Host-side veth interface name. Defaults to "ve-{name}".
        state_path: Override state file path (for testing).

    Returns:
        SubnetAllocation with subnet, IPs, table ID, and priority.

    Raises:
        RuntimeError: If the pool is exhausted (255 subnets).
    """
    state = _load_state(state_path)

    # Check for existing allocation
    for alloc in state.allocations:
        if alloc.container_name == container_name:
            return alloc

    # Find next available third octet
    used = _used_octets(state)
    octet = state.next_octet
    while octet in used:
        octet += 1
        if octet > 255:
            raise RuntimeError("Subnet pool exhausted (255 /30 subnets allocated)")

    # Build the /30 subnet
    subnet = f"10.200.{octet}.0/30"
    host_ip = f"10.200.{octet}.1"
    container_ip = f"10.200.{octet}.2"
    iface = host_iface or f"ve-{container_name}"

    # Allocate table ID and priority
    table_id = state.next_table_id
    priority = state.next_priority

    alloc = SubnetAllocation(
        container_name=container_name,
        subnet=subnet,
        host_ip=host_ip,
        container_ip=container_ip,
        host_iface=iface,
        table_id=table_id,
        priority=priority,
    )

    state.allocations.append(alloc)
    state.next_octet = octet + 1
    state.next_table_id = table_id + 1
    state.next_priority = priority + 1
    _save_state(state, state_path)

    return alloc


def release(container_name: str, state_path: Path | None = None) -> bool:
    """Release a subnet allocation.

    Args:
        container_name: Container to release.
        state_path: Override state file path (for testing).

    Returns:
        True if an allocation was found and released.
    """
    state = _load_state(state_path)
    original_len = len(state.allocations)
    state.allocations = [
        a for a in state.allocations if a.container_name != container_name
    ]
    if len(state.allocations) < original_len:
        _save_state(state, state_path)
        return True
    return False


def get_allocation(
    container_name: str, state_path: Path | None = None
) -> SubnetAllocation | None:
    """Look up an existing allocation by container name."""
    state = _load_state(state_path)
    for alloc in state.allocations:
        if alloc.container_name == container_name:
            return alloc
    return None


def list_allocations(state_path: Path | None = None) -> list[SubnetAllocation]:
    """List all current allocations."""
    state = _load_state(state_path)
    return list(state.allocations)


def to_veth_config(alloc: SubnetAllocation) -> VethConfig:
    """Convert an allocation to a VethConfig for the archetype resolver."""
    return VethConfig(
        host_iface=alloc.host_iface,
        container_ip=alloc.container_ip,
        host_ip=alloc.host_ip,
        subnet=alloc.subnet,
    )
