"""
cortex/hippo/scheduler.py — Adaptive compaction scheduling.

Decides whether and how aggressively to compact based on conversation pace
(message cadence) and token pressure. Replaces the fixed-threshold trigger
with a dynamic urgency score.

Issue: #1191
"""

from __future__ import annotations

import time
from collections import deque
from dataclasses import dataclass
from enum import Enum

__all__ = ["CompactionMode", "CompactionDecision", "CompactionScheduler"]


class CompactionMode(Enum):
    SKIP = "skip"       # too busy, not urgent
    LIGHT = "light"     # compact one leaf chunk
    FULL = "full"       # full sweep (idle + urgent)
    FORCE = "force"     # safety valve, compact regardless


@dataclass
class CompactionDecision:
    mode: CompactionMode
    urgency: float          # 0.0-1.0
    token_pressure: float   # current_tokens / budget
    idle_seconds: float     # time since last message
    reason: str


class CompactionScheduler:
    """Adaptive compaction scheduling based on conversation pace.

    The scheduler combines *token pressure* (how full the context is) with
    an *idle factor* (how long since the last message) to produce an
    *urgency* score.  This lets Hippo skip compaction during rapid-fire
    exchanges and run full sweeps when the user goes quiet.

    Safety valve: when token pressure exceeds ``urgency_bypass`` the
    scheduler always returns FORCE regardless of idle time.
    """

    def __init__(
        self,
        idle_threshold_seconds: float = 30.0,
        urgency_bypass: float = 0.95,
        light_threshold: float = 0.5,
        full_threshold: float = 0.8,
        *,
        _clock: object | None = None,
    ) -> None:
        self._idle_threshold = idle_threshold_seconds
        self._urgency_bypass = urgency_bypass
        self._light_threshold = light_threshold
        self._full_threshold = full_threshold

        # Per-conversation message timestamps
        self._message_times: dict[int, deque] = {}
        self._window_seconds: float = 120.0  # track last 2 minutes

        # Injectable clock for testing (callable returning float)
        self._clock = _clock or time.monotonic

    # ── Recording ─────────────────────────────────────────────────

    def record_message(self, conversation_id: int) -> None:
        """Record a message timestamp for cadence tracking."""
        now = self._clock()
        if conversation_id not in self._message_times:
            self._message_times[conversation_id] = deque(maxlen=100)
        dq = self._message_times[conversation_id]
        dq.append(now)
        # Prune old entries
        cutoff = now - self._window_seconds
        while dq and dq[0] < cutoff:
            dq.popleft()

    # ── Evaluation ────────────────────────────────────────────────

    def evaluate(
        self,
        conversation_id: int,
        current_tokens: int,
        token_budget: int,
    ) -> CompactionDecision:
        """Evaluate whether and how to compact."""
        now = self._clock()
        token_pressure = current_tokens / token_budget if token_budget > 0 else 0.0

        # Calculate idle time
        dq = self._message_times.get(conversation_id, deque())
        idle_seconds = (now - dq[-1]) if dq else float("inf")

        # Safety valve — force compact regardless of pace
        if token_pressure >= self._urgency_bypass:
            return CompactionDecision(
                mode=CompactionMode.FORCE,
                urgency=1.0,
                token_pressure=token_pressure,
                idle_seconds=idle_seconds,
                reason=(
                    f"Safety valve: token pressure {token_pressure:.1%} "
                    f">= {self._urgency_bypass:.1%}"
                ),
            )

        # Calculate urgency = token_pressure × idle_factor
        idle_factor = min(1.0, idle_seconds / self._idle_threshold)
        urgency = token_pressure * idle_factor

        if urgency >= self._full_threshold:
            mode = CompactionMode.FULL
            reason = (
                f"Full sweep: urgency {urgency:.2f} "
                f"(pressure={token_pressure:.1%}, idle={idle_seconds:.0f}s)"
            )
        elif urgency >= self._light_threshold:
            mode = CompactionMode.LIGHT
            reason = f"Light compaction: urgency {urgency:.2f}"
        else:
            mode = CompactionMode.SKIP
            reason = (
                f"Skipping: urgency {urgency:.2f} too low "
                f"(pressure={token_pressure:.1%}, idle={idle_seconds:.0f}s)"
            )

        return CompactionDecision(
            mode=mode,
            urgency=urgency,
            token_pressure=token_pressure,
            idle_seconds=idle_seconds,
            reason=reason,
        )

    # ── Cadence ───────────────────────────────────────────────────

    def get_cadence(self, conversation_id: int) -> float:
        """Get messages per minute for a conversation."""
        dq = self._message_times.get(conversation_id)
        if not dq or len(dq) < 2:
            return 0.0
        span = dq[-1] - dq[0]
        if span <= 0:
            return 0.0
        return (len(dq) - 1) / (span / 60.0)
