#!/usr/bin/env python3
"""
helix_cdc/runtime/se_router.py

Se-based (Symbolic Entropy) router for FibPi runtime.

Instead of pure round-robin, routes jobs to cores based on their Se score.
High-Se jobs (complex patterns) go to different cores than low-Se jobs.

v01 mode: "se_bucket"
    - Maintains running min/max of Se with a window
    - Normalizes Se to [0, 1]
    - Buckets into cores by normalized Se

Usage:
    from helix_cdc.runtime.se_router import SeRouter, SeRouterConfig

    router = SeRouter(SeRouterConfig(cores=[0, 1, 2, 3]))
    core = router.observe(se=0.75)  # Returns core ID based on Se
"""

from __future__ import annotations

from collections import deque
from dataclasses import dataclass, field
from typing import Deque, Dict, List, Optional


@dataclass
class SeRouterConfig:
    """
    Configuration for SeRouter.

    Attributes:
        cores: List of core IDs to route to.
        mode: Routing mode - "se_bucket" for v01.
        window: Window size for running min/max of Se.
        epsilon: Small value to prevent division by zero.
    """
    cores: List[int] = field(default_factory=lambda: [0, 1])
    mode: str = "se_bucket"
    window: int = 256
    epsilon: float = 1e-8


class SeRouter:
    """
    Routes jobs to cores based on Symbolic Entropy (Se) scores.

    v01 "se_bucket" mode:
    - Maintains running min/max of Se over a window
    - Normalizes Se to [0, 1] range
    - Buckets into cores based on normalized value

    Higher Se -> higher core index (typically)
    """

    def __init__(self, config: Optional[SeRouterConfig] = None):
        if config is None:
            config = SeRouterConfig()

        self._config = config
        self._cores = config.cores if config.cores else [0]
        self._mode = config.mode
        self._window = config.window
        self._epsilon = config.epsilon

        # Running statistics
        self._se_history: Deque[float] = deque(maxlen=config.window)
        self._se_min: float = float("inf")
        self._se_max: float = float("-inf")

        # Tracking
        self._observations: int = 0
        self._core_se_sums: Dict[int, float] = {c: 0.0 for c in self._cores}
        self._core_counts: Dict[int, int] = {c: 0 for c in self._cores}

    @property
    def cores(self) -> List[int]:
        """Return copy of core pool."""
        return list(self._cores)

    @property
    def mode(self) -> str:
        """Return routing mode."""
        return self._mode

    @property
    def observations(self) -> int:
        """Return total observations."""
        return self._observations

    def _update_running_stats(self, se: float) -> None:
        """Update running min/max from history window."""
        self._se_history.append(se)

        if len(self._se_history) < self._window:
            # Haven't filled window yet, just track extremes
            self._se_min = min(self._se_min, se)
            self._se_max = max(self._se_max, se)
        else:
            # Recompute from window (handles when old extremes fall out)
            self._se_min = min(self._se_history)
            self._se_max = max(self._se_history)

    def _normalize_se(self, se: float) -> float:
        """Normalize Se to [0, 1] using running min/max."""
        range_se = self._se_max - self._se_min + self._epsilon
        normalized = (se - self._se_min) / range_se
        return max(0.0, min(1.0, normalized))

    def observe(self, se: float) -> int:
        """
        Given an Se score, return chosen core ID.

        Args:
            se: Symbolic entropy value for the job.

        Returns:
            Core ID to route to.
        """
        self._observations += 1
        self._update_running_stats(se)

        if self._mode == "se_bucket":
            # Normalize and bucket
            se_norm = self._normalize_se(se)
            bucket = int(se_norm * len(self._cores))
            # Clamp to valid range
            bucket = min(bucket, len(self._cores) - 1)
            core = self._cores[bucket]
        else:
            # Fallback: round-robin by observation count
            core = self._cores[self._observations % len(self._cores)]

        # Track for stats
        self._core_se_sums[core] += se
        self._core_counts[core] += 1

        return core

    def get_stats(self) -> Dict:
        """
        Return routing statistics.

        Returns:
            Dict with:
                observations: total observations
                core_counts: {core: count}
                mean_se_per_core: {core: mean_se}
                se_range: (min, max)
        """
        mean_se_per_core = {}
        for core in self._cores:
            count = self._core_counts[core]
            if count > 0:
                mean_se_per_core[core] = self._core_se_sums[core] / count
            else:
                mean_se_per_core[core] = 0.0

        return {
            "observations": self._observations,
            "core_counts": dict(self._core_counts),
            "mean_se_per_core": mean_se_per_core,
            "se_range": (
                self._se_min if self._se_min != float("inf") else 0.0,
                self._se_max if self._se_max != float("-inf") else 0.0,
            ),
        }

    def reset(self) -> None:
        """Reset all statistics."""
        self._se_history.clear()
        self._se_min = float("inf")
        self._se_max = float("-inf")
        self._observations = 0
        self._core_se_sums = {c: 0.0 for c in self._cores}
        self._core_counts = {c: 0 for c in self._cores}

    def __repr__(self) -> str:
        return (
            f"SeRouter(cores={self._cores}, mode='{self._mode}', "
            f"observations={self._observations})"
        )


if __name__ == "__main__":
    # Self-test: show Se -> core mapping
    print("SeRouter self-test")
    print("=" * 40)

    router = SeRouter(SeRouterConfig(cores=[0, 1, 2, 3]))
    print(f"Router: {router}")
    print()

    # Test with varied Se scores
    test_scores = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95]

    print("Se score -> Core mapping:")
    for se in test_scores:
        core = router.observe(se)
        print(f"  Se={se:.2f} -> core {core}")

    print()
    stats = router.get_stats()
    print(f"Core counts: {stats['core_counts']}")
    print(f"Mean Se per core: {stats['mean_se_per_core']}")
    print(f"Se range: {stats['se_range']}")
    print()
    print("Self-test complete.")
