"""POST /topologies/blank — create an empty editable topology.

Produces a minimal ``pending`` topology seeded with exactly one DMZ LAN
and its mandatory host-gateway decky.  Intended for the MazeNET editor
landing flow: unlike ``POST /topologies`` (which runs the generator),
this endpoint takes no generator parameters and skips the planner
entirely.  The DMZ+gateway invariant is enforced server-side so the
editor never has to special-case a "no DMZ yet" state.
"""
from __future__ import annotations

import json

from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, Field as PydanticField

from decnet.telemetry import traced as _traced
from decnet.topology.allocator import SubnetAllocator, reserved_subnets
from decnet.web.db.models import TopologySummary
from decnet.web.dependencies import repo, require_admin
from decnet.web.router.topology._target_host import validate_target_host

router = APIRouter()


class BlankTopologyRequest(BaseModel):
    """Body for POST /topologies/blank — name plus optional agent pinning."""
    name: str = PydanticField(..., min_length=1, max_length=64)
    mode: str = PydanticField(default="unihost", pattern=r"^(unihost|agent)$")
    target_host_uuid: str | None = PydanticField(default=None)


@router.post(
    "/blank",
    tags=["MazeNET Topologies"],
    response_model=TopologySummary,
    status_code=status.HTTP_201_CREATED,
    responses={
        400: {"description": "Malformed body or invalid topology name"},
        401: {"description": "Missing or invalid credentials"},
        403: {"description": "Insufficient permissions"},
        409: {"description": "Name collision or subnet pool exhausted"},
    },
)
@_traced("api.topology.create_blank")
async def api_create_blank_topology(
    body: BlankTopologyRequest,
    _admin: dict = Depends(require_admin),
) -> TopologySummary:
    # 0. Validate mode/host pairing before any writes.
    await validate_target_host(repo, body.mode, body.target_host_uuid)

    # 1. Topology row
    try:
        topology_id = await repo.create_topology(
            {
                "name": body.name,
                "mode": body.mode,
                "target_host_uuid": body.target_host_uuid,
                "status": "pending",
                "config_snapshot": json.dumps({"blank": True}),
            }
        )
    except Exception as exc:  # noqa: BLE001 — surface duplicate-name as 409
        raise HTTPException(status_code=409, detail=str(exc)) from exc

    # 2. DMZ LAN with auto-allocated subnet
    try:
        allocator = SubnetAllocator(
            "10.0", reserved=await reserved_subnets(repo)
        )
        subnet = allocator.next_free()
    except RuntimeError as exc:
        raise HTTPException(status_code=409, detail=str(exc)) from exc

    lan_id = await repo.add_lan(
        {
            "topology_id": topology_id,
            "name": "dmz",
            "subnet": subnet,
            "is_dmz": True,
            "x": 40,
            "y": 40,
        }
    )

    # 3. DMZ-gateway decky — a normal multi-homed bridge decky.
    # `forwards_l3=True` turns on net.ipv4.ip_forward + NET_ADMIN at
    # compose time (see decnet/topology/compose.py).  No host-mode,
    # no MACVLAN — the gateway reaches the outside world via Docker
    # port publishing (see composer port emission).
    decky_uuid = await repo.add_topology_decky(
        {
            "topology_id": topology_id,
            "name": "dmz-gateway",
            "services": ["ssh"],
            "decky_config": {
                "archetype": "deaddeck",
                "forwards_l3": True,
            },
            "state": "pending",
            "x": 20,
            "y": 60,
        }
    )

    # 4. Membership edge on the DMZ — is_bridge=True marks this decky
    # as the topology's bridge gateway; forwards_l3 mirrors the decky
    # config so the generator/compose paths stay consistent.
    await repo.add_topology_edge(
        {
            "topology_id": topology_id,
            "decky_uuid": decky_uuid,
            "lan_id": lan_id,
            "is_bridge": True,
            "forwards_l3": True,
        }
    )

    row = await repo.get_topology(topology_id)
    if row is None:  # pragma: no cover — create then vanish
        raise HTTPException(status_code=500, detail="topology insert vanished")
    return row
