"""MazeNET topology config + in-memory generation output."""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional

from pydantic import BaseModel, Field, model_validator


class TopologyConfig(BaseModel):
    """Parameters driving :func:`decnet.topology.generator.generate`."""

    name: str = Field(..., min_length=1, max_length=64)
    mode: str = Field(default="unihost", pattern=r"^(unihost|agent)$")

    # Topology shape
    depth: int = Field(..., ge=1, le=16, description="Max depth from DMZ")
    branching_factor: int = Field(..., ge=1, le=8, description="Max child LANs per LAN")
    deckies_per_lan_min: int = Field(default=1, ge=0, le=32)
    deckies_per_lan_max: int = Field(default=3, ge=1, le=32)

    # Probability a given non-DMZ LAN's connection to its parent uses a
    # bridge decky that forwards L3 (enables attacker pivot).  Bridge
    # existence between parent/child is implicit — every non-DMZ LAN
    # has exactly one parent bridge.  This controls *forwarding*, not
    # the existence of the bridge.
    bridge_forward_probability: float = Field(default=1.0, ge=0.0, le=1.0)

    # Probability of injecting a DAG cross-edge: a decky also bridged
    # from its LAN to a non-parent, non-child LAN.  0.0 yields a tree.
    cross_edge_probability: float = Field(default=0.0, ge=0.0, le=1.0)

    # IP allocation base.  LANs get sequential /24s carved out of this
    # network.  Accepts either a full CIDR (e.g. ``172.16.0.0/12`` for
    # 4096 slots) or the legacy two-octet shorthand ``172.20`` which
    # auto-lifts to ``172.20.0.0/16`` (256 slots).  Default is a /12
    # so mass-scale topologies (depth/branching trees with >256 LANs)
    # don't exhaust the pool on first generation.
    subnet_base_prefix: str = Field(
        default="172.16.0.0/12",
        pattern=r"^\d{1,3}\.\d{1,3}(\.\d{1,3}\.\d{1,3}/\d{1,2})?$",
    )

    # Service selection — reuses decnet.fleet.build_deckies' randomizer.
    randomize_services: bool = Field(default=True)
    services_explicit: Optional[list[str]] = None

    seed: Optional[int] = Field(default=None, ge=0)

    @model_validator(mode="after")
    def _check_min_max(self) -> "TopologyConfig":
        if self.deckies_per_lan_min > self.deckies_per_lan_max:
            raise ValueError(
                "deckies_per_lan_min must be <= deckies_per_lan_max"
            )
        if not self.randomize_services and not self.services_explicit:
            raise ValueError(
                "either randomize_services=True or services_explicit must be set"
            )
        return self


@dataclass
class _PlannedLAN:
    """In-memory LAN record emitted by the generator."""
    name: str
    subnet: str
    is_dmz: bool
    parent: Optional[str]  # name of parent LAN, None for DMZ
    # Canvas coordinates — generator leaves them None; the web editor
    # (or a future auto-layouter) fills them in.
    x: Optional[float] = None
    y: Optional[float] = None


@dataclass
class _PlannedDecky:
    """In-memory decky record emitted by the generator."""
    name: str
    services: list[str]
    # Mapping LAN-name → assigned IP within that LAN's subnet.
    ips_by_lan: dict[str, str] = field(default_factory=dict)
    forwards_l3: bool = False  # only meaningful when present on ≥2 LANs
    # Per-service config overrides: {service_name: {field: value}}.
    # Mirrors ``DeckyConfig.service_config`` from the flat-fleet path;
    # services read these via ``compose_fragment(service_cfg=...)``.
    service_config: dict[str, dict] = field(default_factory=dict)
    # Canvas coordinates — see _PlannedLAN.x/y.
    x: Optional[float] = None
    y: Optional[float] = None


@dataclass
class _PlannedEdge:
    """In-memory (decky, LAN) membership edge."""
    decky_name: str
    lan_name: str
    is_bridge: bool
    forwards_l3: bool


@dataclass
class GeneratedTopology:
    """Full in-memory output of :func:`decnet.topology.generator.generate`.

    Names are unique within the topology.  No UUIDs are assigned here —
    those are minted by :mod:`decnet.topology.persistence` when the
    topology is written to the repo.
    """
    config: TopologyConfig
    lans: list[_PlannedLAN]
    deckies: list[_PlannedDecky]
    edges: list[_PlannedEdge]
