"""Crystallization probe — continuous monitor for Phase 2 -> Phase 3 transitions.

Detects when the soup is approaching crystallization by tracking Shannon entropy
over the tape population distribution and its rate of change. Fires alerts when
diversity loss is sustained.
"""

from dataclasses import dataclass, asdict
from collections import deque

import numpy as np

from .metrics import shannon_entropy, compression_ratio, population_histogram


@dataclass(slots=True)
class ProbeReading:
    """A single probe measurement."""
    interaction: int
    shannon_entropy: float
    compression_ratio: float
    entropy_delta: float      # dH/dt (change since last reading)
    entropy_accel: float      # d^2H/dt^2 (change in the change)
    alert_level: str          # "none" | "warning" | "critical"
    unique_tapes: int

    def to_dict(self) -> dict:
        return asdict(self)


class CrystallizationProbe:
    """Monitors soup diversity and detects crystallization onset.

    Alert logic:
    - warning: majority (>= 50%) of recent entropy deltas are negative,
      AND unique_tapes has dropped below 50% of peak observed
    - critical: warning is active AND entropy_accel < critical_threshold
      (accelerating entropy loss)
    """

    def __init__(self, window_size: int = 5,
                 critical_threshold: float = -0.01):
        self.window_size = window_size
        self.critical_threshold = critical_threshold
        self._history: list[ProbeReading] = []
        self._recent_deltas: deque[float] = deque(maxlen=window_size)
        self._peak_unique: int = 0

    def measure(self, soup: np.ndarray, interaction: int) -> ProbeReading:
        """Take a probe reading from the current soup state."""
        h = shannon_entropy(soup)
        cr = compression_ratio(soup)

        hist = population_histogram(soup)
        unique = len(hist)
        self._peak_unique = max(self._peak_unique, unique)

        # Compute derivatives
        if self._history:
            prev = self._history[-1]
            entropy_delta = h - prev.shannon_entropy
            if len(self._history) >= 2:
                prev_delta = prev.shannon_entropy - self._history[-2].shannon_entropy
                entropy_accel = entropy_delta - prev_delta
            else:
                entropy_accel = 0.0
        else:
            entropy_delta = 0.0
            entropy_accel = 0.0

        self._recent_deltas.append(entropy_delta)

        # Alert logic
        alert_level = "none"
        if len(self._recent_deltas) >= self.window_size:
            n_negative = sum(1 for d in self._recent_deltas if d < 0)
            majority_decreasing = n_negative >= len(self._recent_deltas) // 2 + 1
            diversity_collapsed = (self._peak_unique > 0
                                   and unique < self._peak_unique * 0.5)

            if majority_decreasing and diversity_collapsed:
                alert_level = "warning"
                if entropy_accel < self.critical_threshold:
                    alert_level = "critical"

        reading = ProbeReading(
            interaction=interaction,
            shannon_entropy=h,
            compression_ratio=cr,
            entropy_delta=entropy_delta,
            entropy_accel=entropy_accel,
            alert_level=alert_level,
            unique_tapes=unique,
        )
        self._history.append(reading)
        return reading

    def get_history(self) -> list[ProbeReading]:
        """Return all probe readings taken so far."""
        return list(self._history)
