"""Unified threshold system for syrin.

Provides separate threshold classes for different use cases.

**Threshold concepts:**
- **metric:** What is measured — COST (USD), TOKENS (token usage), RPM/TPM/RPD (rate limits).
- **window:** Over what scope the limit applies — RUN (this request), HOUR, DAY, WEEK, MONTH, or
  MAX_TOKENS (context window utilization). BudgetThreshold uses run|hour|day|week|month;
  ContextThreshold uses MAX_TOKENS only.

Example:
    >>> from syrin import Budget
    >>> from syrin.threshold import BudgetThreshold
    >>>
    >>> budget = Budget(
    ...     max_cost=10.0,
    ...     thresholds=[
    ...         BudgetThreshold(at=80, action=lambda ctx: print(f"At {ctx.percentage}%"))
    ...     ]
    ... )
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TypeVar

from syrin.enums import ThresholdMetric, ThresholdWindow


def _noop_compact() -> None:
    """No-op compactor when not inside context prepare."""
    pass


def compact_if_available(ctx: ThresholdContext) -> None:
    """Threshold action: run context compaction when triggered (e.g. during prepare). No-op otherwise.

    Use as the action for ContextThreshold when you want to compact at a utilization percentage
    without writing a lambda. Only has effect when the context manager is inside prepare (when
    the threshold is evaluated); otherwise ctx.compact() is a no-op.

    Example:
        >>> from syrin.threshold import ContextThreshold, compact_if_available
        >>> ContextThreshold(at=75, action=compact_if_available)
    """
    ctx.compact()


@dataclass
class ThresholdContext:
    """Event object passed to threshold actions (Budget, Context, RateLimit).

    Use when you need to react at a utilization percentage (e.g. compact at 75%).
    For context thresholds, call compact() to run compaction; no-op when not in prepare.

    Attributes:
        percentage: Utilization percentage (0-100) that triggered this threshold.
        metric: Metric tracked (e.g. ThresholdMetric.TOKENS, ThresholdMetric.COST).
        current_value: Current value (tokens used, cost so far).
        limit_value: Limit or cap (max_tokens, run budget).
        budget_run: Alias for limit_value when metric is COST.
        parent: Parent object (Agent, Budget) when available.
        metadata: Extra key-value data.
        compact: (Context only.) Call to compact; no-op when not inside prepare.
    """

    percentage: int
    """Utilization percentage (0-100) that triggered this threshold."""
    metric: object  # ThresholdMetric
    """Metric being tracked (e.g. ThresholdMetric.TOKENS, ThresholdMetric.COST)."""
    current_value: float
    """Current value (e.g. tokens used, cost so far)."""
    limit_value: float
    """Limit or cap (e.g. max_tokens, run budget)."""
    budget_run: float = 0.0
    """Alias for limit_value (for COST metric)."""
    parent: object = None
    """Parent object (e.g. Agent, Budget) when available."""
    metadata: dict[str, object] = field(default_factory=dict)
    """Extra key-value data."""
    compact: Callable[[], None] = field(default=_noop_compact)
    """(Context only.) Call to compact; no-op when not inside prepare."""


# Alias: event passed to threshold action (avoids confusion with Context config).


# Type alias for threshold action handlers: callable receiving ThresholdContext, returns None.
from collections.abc import Callable as _Callable

ThresholdAction = _Callable[["ThresholdContext"], None]


T = TypeVar("T", bound="BaseThreshold")


@dataclass
class BaseThreshold:
    """Base class for BudgetThreshold, ContextThreshold, RateLimitThreshold.

    Attributes:
        at: Percentage (0-100) at which to trigger (when pct >= at).
        at_range: Optional (min, max); trigger only when min <= pct <= max.
        action: Callable receiving ThresholdContext when triggered.
        metric: ThresholdMetric (COST, TOKENS, RPM, etc.).
    """

    at: int = 0
    action: Callable[[ThresholdContext], None] | None = None
    metric: object = None  # ThresholdMetric
    at_range: tuple[int, int] | None = None  # (min, max): trigger when min <= pct <= max

    def __post_init__(self) -> None:
        if self.at_range is not None:
            lo, hi = self.at_range
            if not 0 <= lo <= hi <= 100:
                raise ValueError(
                    f"Threshold 'at_range' must be (0-100, 0-100) with lo<=hi, got {self.at_range}"
                )
        else:
            if not 0 <= self.at <= 100:
                raise ValueError(f"Threshold 'at' must be between 0 and 100, got {self.at}")
        if self.action is None:
            raise ValueError("Threshold 'action' is required")

    def should_trigger(self, percentage: int, metric: object = None) -> bool:
        """Check if this threshold should trigger."""
        if self.at_range is not None:
            lo, hi = self.at_range
            in_range = lo <= percentage <= hi
        else:
            in_range = percentage >= self.at
        if metric is not None and hasattr(self.metric, "value") and hasattr(metric, "value"):
            return in_range and str(self.metric.value) == str(metric.value)
        return in_range

    def execute(self, ctx: ThresholdContext) -> None:
        if self.action is not None:
            self.action(ctx)


@dataclass
class BudgetThreshold(BaseThreshold):
    """Threshold for Budget (cost in USD or tokens).

    Args:
        at: Percentage (0-100) at which to trigger (use when pct >= at).
        at_range: Optional (min, max): trigger only when min <= pct <= max.
        action: Function to call when threshold is crossed. Receives ThresholdContext.
        metric: ThresholdMetric.COST (default) or ThresholdMetric.TOKENS.
        window: ThresholdWindow.RUN (default), HOUR, DAY, WEEK, or MONTH.
            For TOKENS, limits come from TokenLimits (run and per=TokenRateLimit).

    Example:
        >>> BudgetThreshold(at=80, action=lambda ctx: print(f"At {ctx.percentage}%"))
        >>> BudgetThreshold(at=80, action=warn, window=ThresholdWindow.DAY)
        >>> BudgetThreshold(at=80, metric=ThresholdMetric.TOKENS, window=ThresholdWindow.HOUR, action=...)
        >>> BudgetThreshold(at_range=(70, 75), action=alert)
    """

    metric: object = ThresholdMetric.COST
    window: ThresholdWindow = ThresholdWindow.RUN

    def __post_init__(self) -> None:
        super().__post_init__()
        if isinstance(self.window, str):
            self.window = ThresholdWindow(self.window)
        if self.window not in (
            ThresholdWindow.RUN,
            ThresholdWindow.HOUR,
            ThresholdWindow.DAY,
            ThresholdWindow.WEEK,
            ThresholdWindow.MONTH,
        ):
            raise ValueError(
                f"BudgetThreshold window must be ThresholdWindow.RUN/HOUR/DAY/WEEK/MONTH, got {self.window!r}"
            )


@dataclass
class ContextThreshold(BaseThreshold):
    """Threshold for Context (tracks token usage vs max_tokens).

    Same shape as BudgetThreshold for consistency: at, at_range, action, metric, window.
    Context only supports window=MAX_TOKENS (current context window).

    Args:
        at: Percentage (0-100) at which to trigger (use when pct >= at).
        at_range: Optional (min, max): trigger only when min <= pct <= max.
        action: Function to call when threshold is crossed. Receives ThresholdContext.
            Use ctx.compact() inside the action to compact context when triggered.
        metric: ThresholdMetric.TOKENS (fixed for context).
        window: ThresholdWindow.MAX_TOKENS only (current context window).

    Example:
        >>> from syrin.threshold import ContextThreshold, compact_if_available
        >>> ContextThreshold(at=80, action=lambda ctx: print(f"At {ctx.percentage}%"))
        >>> ContextThreshold(at=75, action=compact_if_available)
        >>> ContextThreshold(at_range=(70, 75), action=warn)
    """

    metric: object = ThresholdMetric.TOKENS
    window: object = ThresholdWindow.MAX_TOKENS

    def __post_init__(self) -> None:
        super().__post_init__()
        w = self.window if hasattr(self.window, "value") else ThresholdWindow(str(self.window))
        if w != ThresholdWindow.MAX_TOKENS:
            raise ValueError(
                f"ContextThreshold window must be ThresholdWindow.MAX_TOKENS, got {self.window!r}"
            )


@dataclass
class RateLimitThreshold(BaseThreshold):
    """Threshold for APIRateLimit (tracks RPM, TPM, or RPD).

    Requires metric to be specified. Use the action callback to do anything when
    the threshold is crossed (e.g. raise, wait, switch model, log).

    Args:
        at: Percentage (0-100) at which to trigger
        action: Function to call when threshold is crossed. Receives ThresholdContext.
        metric: The rate limit metric (ThresholdMetric.RPM, TPM, or RPD)

    Example:
        >>> from syrin.threshold import RateLimitThreshold
        >>> from syrin.enums import ThresholdMetric
        >>>
        >>> def on_threshold(ctx):
        ...     if ctx.percentage >= 80:
        ...         raise RuntimeError("Rate limit threshold reached")
        >>> RateLimitThreshold(at=80, action=on_threshold, metric=ThresholdMetric.RPM)
    """

    metric: object = None

    def __post_init__(self) -> None:
        super().__post_init__()
        if self.metric is None:
            raise ValueError(
                "RateLimitThreshold 'metric' is required - use ThresholdMetric.RPM, TPM, or RPD"
            )
        valid_metrics = {ThresholdMetric.RPM, ThresholdMetric.TPM, ThresholdMetric.RPD}
        metric_val = self.metric.value if hasattr(self.metric, "value") else self.metric
        if metric_val not in valid_metrics:
            raise ValueError(f"RateLimitThreshold only supports {valid_metrics}, got {metric_val}")


__all__ = [
    "ThresholdContext",
    "ThresholdAction",
    "BaseThreshold",
    "BudgetThreshold",
    "ThresholdWindow",
    "ContextThreshold",
    "RateLimitThreshold",
    "compact_if_available",
]
