"""Track-B container-simulated house pools  —  DRAFT (next experiment only).

Purpose (see prereg NEXT-EXPERIMENT-DRAFT §1 Track B): replace the *logical phone
labels* used to stand in for house-B/house-C in the sealed battery with **genuinely
distinct container pools**. Each house becomes an isolated Docker network hosting its
own N node containers, each with its own X25519/Ed25519 identity — giving real,
auditable node-distinctness on a single laptop, with **no phone dependency**.

Containment (load-bearing, identical to the sealed instrument):
  * Every node runs inside an **isolated engine** — this module refuses to place a node
    unless ``engine != "local"``. No node ever runs on the bare host.
  * Self-generated fixture traffic only; no external target; lab-only.

Integrity boundary: anything measured through these pools is a **new** measurement. It
may be reported as *exploratory*, or promoted to confirmatory only under a fresh frozen
pre-registration. It may NEVER be merged into the sealed original battery.

This module is a pool *model* + containment validator + deterministic identity
assignment. Actual container launch is injected by the caller (``executor.py``) via a
``launch_fn`` seam, so this file stays correct without duplicating engine plumbing.
"""

from __future__ import annotations

import hashlib
from dataclasses import dataclass, field
from typing import Callable, Dict, List, Optional


class ContainmentError(RuntimeError):
    """Raised when a placement would violate the isolated-engine law."""


@dataclass(frozen=True)
class SimNode:
    """One simulated relay node: an isolated container with its own identity."""

    node_id: str          # deterministic, seed-derived
    house: str            # owning house id (e.g. "house-B")
    engine: str           # MUST be an isolated engine (docker/multipass), never "local"
    net: str              # isolated docker network the node lives on

    def __post_init__(self) -> None:
        if self.engine == "local":
            raise ContainmentError(
                f"node {self.node_id}: engine=='local' is forbidden — "
                "isolated engine only (containment law §1)"
            )


@dataclass
class HousePool:
    """A house realised as a distinct, isolated container pool (Track B)."""

    house: str
    engine: str
    net: str
    size: int
    nodes: List[SimNode] = field(default_factory=list)


def _derive_node_id(s0: int, house: str, idx: int) -> str:
    """Deterministic node id from the base seed — mirrors the sealed seed formula."""
    h = hashlib.sha256(f"{s0}|{house}|{idx}".encode()).digest()
    return h[:8].hex()


def plan_house(house: str, size: int, engine: str, s0: int) -> HousePool:
    """Plan a house pool of ``size`` distinct isolated nodes. Does not launch anything.

    Raises ``ContainmentError`` if asked to plan on the local (non-isolated) engine.
    """
    if engine == "local":
        raise ContainmentError(
            f"{house}: refuse to plan a pool on the local engine — isolated only"
        )
    net = f"sor-{house}-net"
    pool = HousePool(house=house, engine=engine, net=net, size=size)
    for i in range(size):
        pool.nodes.append(
            SimNode(node_id=_derive_node_id(s0, house, i), house=house, engine=engine, net=net)
        )
    return pool


def plan_federation(
    house_sizes: Dict[str, int], engine: str, s0: int
) -> Dict[str, HousePool]:
    """Plan several distinct house pools that will be federated.

    Each house gets its **own** isolated network + identities, so cross-house
    split-knowledge is a real property of the topology, not a label.
    """
    return {h: plan_house(h, n, engine, s0) for h, n in house_sizes.items()}


def assert_distinct(pools: Dict[str, HousePool]) -> None:
    """Fail loudly unless every node id is globally unique and every net is per-house."""
    seen: set[str] = set()
    nets: set[str] = set()
    for pool in pools.values():
        if pool.net in nets:
            raise ContainmentError(f"network {pool.net} reused across houses")
        nets.add(pool.net)
        for n in pool.nodes:
            if n.node_id in seen:
                raise ContainmentError(f"duplicate node id {n.node_id}")
            seen.add(n.node_id)


def realise(
    pools: Dict[str, HousePool],
    launch_fn: Callable[[SimNode], str],
) -> Dict[str, List[str]]:
    """Launch every planned node via the injected ``launch_fn`` (executor seam).

    ``launch_fn`` must actually start the node inside its isolated engine/network and
    return a container handle. This function only enforces the containment invariant and
    the distinctness check before delegating; it never shells out to docker itself.
    """
    assert_distinct(pools)
    handles: Dict[str, List[str]] = {}
    for house, pool in pools.items():
        handles[house] = []
        for node in pool.nodes:
            if node.engine == "local":  # belt-and-braces
                raise ContainmentError(f"{node.node_id}: local engine at launch")
            handles[house].append(launch_fn(node))
    return handles
