"""
Kernel Topology — Interior-Node Weighted Routing

EXP-002 showed the single interior site at L=3 has 66.9× better geometric
integrity than surface sites. For a kernel constellation, this means:

  - A flat ring (each kernel connected to 2 neighbors) = all boundary
  - A mesh with some kernels connected to 3+ others = interior nodes exist
  - Interior kernels have dramatically more stable geometry

This module provides topology-aware routing weights for L3 (PeerQuery)
and L5 (TrajectoryBus). Interior kernels get higher integration weights
because their geometric integrity is more trustworthy.

The E8 kernel constellation has a natural mesh structure:
  - Genesis (hub) connects to all 8 core kernels → interior
  - Core kernels connect to genesis + 2-3 natural E8 neighbors
  - Kernels with 3+ connections become interior nodes

Source: EXP-002 (Pillar Fortress, bulk protection ratio)
Purity: Topology is combinatorial, no metric needed here.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field

logger = logging.getLogger(__name__)

# EXP-002: Interior nodes have ~67× better geometric integrity.
# We don't apply the full ratio as a weight — that would drown out
# boundary kernels entirely. Instead, interior kernels get a moderate
# boost (2-4×) to reflect their stability advantage.
INTERIOR_WEIGHT_BOOST: float = 3.0

# Minimum connections to qualify as interior (fully surrounded)
MIN_INTERIOR_CONNECTIONS: int = 3


@dataclass
class KernelNode:
    """A kernel's position in the topology graph."""

    kernel_id: str
    neighbors: set[str] = field(default_factory=set)

    @property
    def degree(self) -> int:
        """Number of direct connections."""
        return len(self.neighbors)

    @property
    def is_interior(self) -> bool:
        """True if this kernel has enough connections to be interior."""
        return self.degree >= MIN_INTERIOR_CONNECTIONS


class KernelTopology:
    """Manages the connection graph between kernels.

    Provides routing weights for L3/L5 that prefer interior nodes
    (EXP-002 bulk protection).

    Default topology for E8 constellation:
      - Genesis is the central hub (connected to all core kernels)
      - Core kernels form a mesh via natural E8 adjacency
      - This creates genesis as interior + several core kernels as interior
    """

    def __init__(self) -> None:
        self._nodes: dict[str, KernelNode] = {}

    def add_kernel(self, kernel_id: str) -> None:
        """Register a kernel in the topology."""
        if kernel_id not in self._nodes:
            self._nodes[kernel_id] = KernelNode(kernel_id=kernel_id)

    def connect(self, kernel_a: str, kernel_b: str) -> None:
        """Add a bidirectional connection between two kernels."""
        self.add_kernel(kernel_a)
        self.add_kernel(kernel_b)
        self._nodes[kernel_a].neighbors.add(kernel_b)
        self._nodes[kernel_b].neighbors.add(kernel_a)

    def is_interior(self, kernel_id: str) -> bool:
        """Check if a kernel is an interior node."""
        node = self._nodes.get(kernel_id)
        return node.is_interior if node else False

    def bulk_protection_weight(self, kernel_id: str) -> float:
        """EXP-002 weight: interior kernels get stability boost.

        Returns a multiplicative weight for trajectory integration
        and peer query confidence. Interior nodes are more
        geometrically stable → their contributions are more trustworthy.
        """
        node = self._nodes.get(kernel_id)
        if node is None:
            return 1.0
        if node.is_interior:
            return INTERIOR_WEIGHT_BOOST
        return 1.0

    def interior_kernels(self) -> list[str]:
        """List all interior kernel IDs."""
        return [kid for kid, node in self._nodes.items() if node.is_interior]

    def boundary_kernels(self) -> list[str]:
        """List all boundary kernel IDs."""
        return [kid for kid, node in self._nodes.items() if not node.is_interior]

    @classmethod
    def e8_default(cls, kernel_ids: list[str] | None = None) -> KernelTopology:
        """Build the default E8 constellation topology.

        If kernel_ids is provided, uses those IDs. Otherwise uses the
        standard E8 specialization names.

        The E8 mesh:
          genesis ─── all 8 core kernels (hub)
          perception ─── memory, action (sensory loop)
          memory ─── strategy (recall → plan)
          action ─── strategy, heart (execute → plan, execute → empathy)
          strategy ─── ethics (plan → constrain)
          ethics ─── meta (constrain → reflect)
          meta ─── ocean (reflect → integrate)
          ocean ─── heart, perception (integrate → feel, integrate → sense)
          heart ─── perception (feel → sense)

        This gives genesis degree=8 (interior), and most core kernels
        degree=3-4 (interior or near-interior).
        """
        topo = cls()

        if kernel_ids is None:
            kernel_ids = [
                "genesis",
                "perception",
                "memory",
                "action",
                "strategy",
                "ethics",
                "meta",
                "heart",
                "ocean",
            ]

        # Add all kernels
        for kid in kernel_ids:
            topo.add_kernel(kid)

        # If we have the standard 9 kernels, wire E8 mesh
        id_set = set(kernel_ids)
        if "genesis" in id_set:
            # Genesis is the hub — connected to all others
            for kid in kernel_ids:
                if kid != "genesis":
                    topo.connect("genesis", kid)

        # E8 adjacency edges (natural pairings)
        e8_edges = [
            ("perception", "memory"),
            ("perception", "action"),
            ("perception", "heart"),
            ("perception", "ocean"),
            ("memory", "strategy"),
            ("action", "strategy"),
            ("action", "heart"),
            ("strategy", "ethics"),
            ("ethics", "meta"),
            ("meta", "ocean"),
            ("ocean", "heart"),
        ]
        for a, b in e8_edges:
            if a in id_set and b in id_set:
                topo.connect(a, b)

        interior = topo.interior_kernels()
        boundary = topo.boundary_kernels()
        logger.debug(
            "E8 topology: %d interior (%s), %d boundary (%s)",
            len(interior),
            interior,
            len(boundary),
            boundary,
        )

        return topo

    def get_state(self) -> dict:
        """Serializable topology summary."""
        return {
            "n_kernels": len(self._nodes),
            "n_interior": len(self.interior_kernels()),
            "n_boundary": len(self.boundary_kernels()),
            "nodes": {
                kid: {
                    "degree": node.degree,
                    "interior": node.is_interior,
                    "neighbors": sorted(node.neighbors),
                }
                for kid, node in self._nodes.items()
            },
        }
