"""
helix_cdc/brainstem/state_detector.py
=====================================

Brainstem state detection: observe telemetry -> detect neural band -> output state.

The brainstem doesn't solve QUBOs. It outputs STATE that becomes QUBO parameters.

Interface A Contract:
    BrainstemInput -> StateDetector.detect() -> BrainstemState

BrainstemState includes:
    - band: Neural band (theta/alpha/beta/gamma)
    - band_confidence: How confident the detection is
    - lobe_votes: Trio dispatch votes (if active)
    - QUBO priors: latency_weight, fidelity_weight, determinism_weight
    - Hard constraints: banned_backends, required_specialists
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Dict, List, Optional
import time


@dataclass
class BrainstemInput:
    """
    Inputs to the brainstem state detector.

    These are observations the brainstem uses to determine the current state.
    """
    # Telemetry
    cpu_load: float = 0.5           # 0.0-1.0 normalized
    gpu_temp_c: float = 50.0        # Celsius
    memory_pressure: float = 0.5    # 0.0-1.0 (higher = more pressure)

    # Task intent
    task_type: str = "inference"    # "inference" | "compression" | "regen"
    se_estimate: float = 0.5        # 0.0-1.0 (symbolic entropy estimate)

    # Requirements
    determinism_required: bool = True
    latency_budget_ms: float = 500.0
    fidelity_band: str = "hybrid_cdna"  # "lossless" | "hybrid_cdna" | "world_b"

    # Optional: external band override (for testing/demo)
    forced_band: Optional[str] = None

    # Timestamp
    timestamp_unix: float = field(default_factory=time.time)


@dataclass
class BrainstemState:
    """
    Output state from the brainstem.

    This state is used to configure the QUBO planner via Interface A.
    """
    # Neural band (from mental state detector)
    band: str = "alpha"             # "theta" | "alpha" | "beta" | "gamma"
    band_confidence: float = 0.8    # 0.0-1.0

    # Lobe votes (if trio mode active)
    lobe_votes: Dict[str, float] = field(default_factory=lambda: {
        "left": 0.33,   # Logical/analytical
        "right": 0.33,  # Creative/intuitive
        "poet": 0.34,   # Expressive/artistic
    })

    # QUBO priors (weights to bias optimization)
    latency_weight: float = 0.5     # Higher = prefer fast backends
    fidelity_weight: float = 0.5    # Higher = prefer lossless paths
    determinism_weight: float = 0.5 # Higher = penalize non-deterministic

    # Hard constraints
    banned_backends: List[str] = field(default_factory=list)  # e.g., ["qpu"]
    required_specialists: List[str] = field(default_factory=list)

    # Diagnostic info
    detection_reason: str = ""
    timestamp_unix: float = field(default_factory=time.time)


class StateDetector:
    """
    Detects brainstem state from input telemetry.

    Band Detection Logic:
    - theta: Relaxed, low load, creative exploration
    - alpha: Reflective, moderate load, balanced
    - beta: Problem-solving, high load, focused
    - gamma: Insight moment, very high activity

    Usage:
        detector = StateDetector()
        input = BrainstemInput(cpu_load=0.8, task_type="inference")
        state = detector.detect(input)
        print(f"Band: {state.band}, Latency weight: {state.latency_weight}")
    """

    # Band thresholds based on activity level
    BAND_THRESHOLDS = {
        "theta": (0.0, 0.25),   # 0-25% activity
        "alpha": (0.25, 0.5),   # 25-50% activity
        "beta": (0.5, 0.75),    # 50-75% activity
        "gamma": (0.75, 1.0),   # 75-100% activity
    }

    # Task type influences band tendency
    TASK_BAND_BIAS = {
        "inference": 0.0,      # Neutral
        "compression": 0.1,    # Slightly higher activity
        "regen": -0.1,         # Slightly lower (deterministic)
        "exploration": -0.2,   # Lower (theta-friendly)
        "analysis": 0.2,       # Higher (beta-friendly)
    }

    # Thermal risk thresholds
    THERMAL_WARNING_C = 70.0
    THERMAL_CRITICAL_C = 85.0

    def __init__(self):
        self._last_state: Optional[BrainstemState] = None

    def detect(self, input: BrainstemInput) -> BrainstemState:
        """
        Detect brainstem state from input.

        Args:
            input: BrainstemInput with telemetry and task info

        Returns:
            BrainstemState with band, weights, and constraints
        """
        # Check for forced band override (testing)
        if input.forced_band is not None:
            band = input.forced_band
            band_confidence = 1.0
            detection_reason = f"forced_override:{band}"
        else:
            # Calculate activity level
            activity = self._compute_activity_level(input)
            band, band_confidence = self._detect_band(activity)
            detection_reason = f"activity={activity:.2f}"

        # Compute lobe votes based on task type
        lobe_votes = self._compute_lobe_votes(input)

        # Map band to QUBO weights
        latency_weight, fidelity_weight, determinism_weight = self._band_to_weights(band)

        # Detect hard constraints
        banned_backends = self._detect_banned_backends(input)
        required_specialists = self._detect_required_specialists(input)

        state = BrainstemState(
            band=band,
            band_confidence=band_confidence,
            lobe_votes=lobe_votes,
            latency_weight=latency_weight,
            fidelity_weight=fidelity_weight,
            determinism_weight=determinism_weight,
            banned_backends=banned_backends,
            required_specialists=required_specialists,
            detection_reason=detection_reason,
            timestamp_unix=time.time(),
        )

        self._last_state = state
        return state

    def _compute_activity_level(self, input: BrainstemInput) -> float:
        """
        Compute overall activity level from telemetry.

        Returns:
            Activity level [0, 1]
        """
        # Base activity from CPU load and memory pressure
        activity = (input.cpu_load * 0.6 + input.memory_pressure * 0.4)

        # Apply task bias
        bias = self.TASK_BAND_BIAS.get(input.task_type, 0.0)
        activity += bias

        # Se estimate influences activity (high Se = more complex)
        activity += (input.se_estimate - 0.5) * 0.2

        # Latency pressure increases activity
        if input.latency_budget_ms < 100:
            activity += 0.3  # Very tight budget -> gamma territory
        elif input.latency_budget_ms < 300:
            activity += 0.15  # Tight budget -> beta territory

        return max(0.0, min(1.0, activity))

    def _detect_band(self, activity: float) -> tuple[str, float]:
        """
        Map activity level to neural band.

        Returns:
            (band_name, confidence)
        """
        for band, (low, high) in self.BAND_THRESHOLDS.items():
            if low <= activity < high:
                # Confidence is higher when activity is closer to band center
                center = (low + high) / 2
                distance_from_center = abs(activity - center)
                max_distance = (high - low) / 2
                confidence = 1.0 - (distance_from_center / max_distance) * 0.4
                return band, confidence

        # Default to gamma if activity >= 1.0
        return "gamma", 0.9

    def _band_to_weights(self, band: str) -> tuple[float, float, float]:
        """
        Map band to QUBO weight triple.

        From the plan:
        | Band  | Latency | Fidelity | Determinism |
        |-------|---------|----------|-------------|
        | Theta | 0.1     | 0.3      | 0.5         |
        | Alpha | 0.3     | 0.7      | 0.6         |
        | Beta  | 0.8     | 0.5      | 0.7         |
        | Gamma | 0.9     | 0.4      | 0.3         |

        Returns:
            (latency_weight, fidelity_weight, determinism_weight)
        """
        weights = {
            "theta": (0.1, 0.3, 0.5),  # Slow ok, explore
            "alpha": (0.3, 0.7, 0.6),  # Reflection needs fidelity
            "beta": (0.8, 0.5, 0.7),   # Problem-solving = fast
            "gamma": (0.9, 0.4, 0.3),  # Insight = speed, can tolerate variance
        }
        return weights.get(band, (0.5, 0.5, 0.5))

    def _compute_lobe_votes(self, input: BrainstemInput) -> Dict[str, float]:
        """
        Compute lobe votes based on task type.

        The trio (left/right/poet) dispatch differently for different tasks.
        """
        if input.task_type == "inference":
            # Balanced for inference
            return {"left": 0.4, "right": 0.3, "poet": 0.3}
        elif input.task_type == "compression":
            # Left-brain dominant for compression (logical)
            return {"left": 0.6, "right": 0.2, "poet": 0.2}
        elif input.task_type == "regen":
            # Right-brain for regeneration (pattern recognition)
            return {"left": 0.3, "right": 0.5, "poet": 0.2}
        elif input.task_type == "exploration":
            # Poet-brain for exploration (creative)
            return {"left": 0.2, "right": 0.3, "poet": 0.5}
        else:
            return {"left": 0.33, "right": 0.33, "poet": 0.34}

    def _detect_banned_backends(self, input: BrainstemInput) -> List[str]:
        """
        Detect which backends should be banned based on constraints.
        """
        banned = []

        # Ban GPU if thermal risk is too high
        if input.gpu_temp_c >= self.THERMAL_CRITICAL_C:
            banned.append("gpu")

        # Ban QPU if determinism is strictly required (QPU has some variance)
        # Note: QPU is deterministic in math but may have sampling variance
        if input.determinism_required and input.fidelity_band == "lossless":
            banned.append("qpu")

        return banned

    def _detect_required_specialists(self, input: BrainstemInput) -> List[str]:
        """
        Detect which specialists are required based on fidelity band.
        """
        required = []

        # Lossless mode requires specific specialists
        if input.fidelity_band == "lossless":
            required.append("sidecar_decoder")

        return required

    @property
    def last_state(self) -> Optional[BrainstemState]:
        """Get the last detected state."""
        return self._last_state
