"""Safe OLT integration boundary.

The registry contains only explicitly registered adapters.  Configuration
rendering is separate from hardware I/O; the bundled ZTE adapter renders text
only and never connects to an OLT.
"""
from dataclasses import dataclass, field
from typing import Any, Callable, Protocol

from app.core.zte_c320 import generate_zte_c320_config


class UnsupportedOLTError(ValueError):
    """Raised when no safe adapter exists for a vendor/model pair."""


class ONTNotFoundError(LookupError):
    """The adapter could not discover the requested serial number."""


@dataclass(frozen=True)
class ONTDiscovery:
    serial_number: str
    brand: str
    port: str


class OLTDiscoveryAdapter(Protocol):
    """The minimum hardware boundary needed by provisioning."""

    def discover_ont(self, serial_number: str) -> ONTDiscovery: ...

    def allocate_onu_id(self, port: str, used_ids: set[int] | None = None) -> int: ...


@dataclass(frozen=True)
class OLTAdapter:
    vendor: str
    model: str
    config_generator: Callable[[Any], str]
    discovery: OLTDiscoveryAdapter | None = None

    def generate_config(self, data: Any) -> str:
        return self.config_generator(data)

    def discover_ont(self, serial_number: str) -> ONTDiscovery:
        if self.discovery is None:
            raise UnsupportedOLTError(
                f"Adapter OLT {self.vendor} {self.model} belum mendukung discovery hardware."
            )
        return self.discovery.discover_ont(serial_number)

    def allocate_onu_id(self, port: str, used_ids: set[int] | None = None) -> int:
        if self.discovery is None:
            raise UnsupportedOLTError(
                f"Adapter OLT {self.vendor} {self.model} belum mendukung alokasi ONU ID hardware."
            )
        return self.discovery.allocate_onu_id(port, used_ids)


@dataclass
class MockOLTAdapter:
    """Deterministic simulation; it never opens a socket or claims hardware success."""

    vendor: str = "MOCK"
    model: str = "SIMULATOR"
    onts: dict[str, ONTDiscovery] = field(default_factory=dict)
    used_ids_by_port: dict[str, set[int]] = field(default_factory=dict)
    min_onu_id: int = 1
    max_onu_id: int = 128

    def discover_ont(self, serial_number: str) -> ONTDiscovery:
        try:
            return self.onts[serial_number.strip().upper()]
        except KeyError as exc:
            raise ONTNotFoundError(f"ONT dengan SN {serial_number} tidak ditemukan di simulator.") from exc

    def allocate_onu_id(self, port: str, used_ids: set[int] | None = None) -> int:
        occupied = set(self.used_ids_by_port.get(port, set())) | set(used_ids or set())
        for onu_id in range(self.min_onu_id, self.max_onu_id + 1):
            if onu_id not in occupied:
                self.used_ids_by_port.setdefault(port, set()).add(onu_id)
                return onu_id
        raise RuntimeError(f"Tidak ada ONU ID kosong pada port {port}.")


_REGISTRY: dict[tuple[str, str], OLTAdapter] = {}


def _key(vendor: str | None, model: str | None) -> tuple[str, str]:
    return ((vendor or "").strip().lower(), (model or "").strip().lower())


def register_olt_adapter(
    vendor: str,
    model: str,
    config_generator: Callable[[Any], str],
    discovery: OLTDiscoveryAdapter | None = None,
) -> None:
    """Register an adapter. Discovery is opt-in and must be supplied explicitly."""
    _REGISTRY[_key(vendor, model)] = OLTAdapter(vendor, model, config_generator, discovery)


def get_olt_adapter(vendor: str | None, model: str | None) -> OLTAdapter:
    adapter = _REGISTRY.get(_key(vendor, model))
    if adapter is None:
        label = f"{vendor or 'vendor kosong'} {model or 'model kosong'}"
        raise UnsupportedOLTError(
            f"Adapter OLT untuk {label} belum tersedia. Perangkat tidak akan dikirimi konfigurasi."
        )
    return adapter


def supported_olt_adapters() -> list[dict[str, str]]:
    return [{"vendor": a.vendor, "model": a.model} for a in _REGISTRY.values()]


register_olt_adapter("ZTE", "C320", generate_zte_c320_config)
