"""Configurable alerting engine -- evaluate metric conditions and track state transitions.

Parses simple metric expressions (``metric_name operator threshold``) safely
without ``eval()``, tracks alert state machines (OK -> PENDING -> FIRING ->
RESOLVED -> OK), and supports duration-based firing delays.
"""

from __future__ import annotations

import logging
import operator
import re
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from collections.abc import Callable

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Supported comparison operators (no eval -- explicit map)
# ---------------------------------------------------------------------------

_OPERATORS: dict[str, Callable[[float, float], bool]] = {
    ">": operator.gt,
    ">=": operator.ge,
    "<": operator.lt,
    "<=": operator.le,
    "==": operator.eq,
    "!=": operator.ne,
}

# Regex for ``metric_name operator threshold`` (e.g. ``backend_latency_ms > 500``)
_CONDITION_RE = re.compile(
    r"^(?P<metric>[a-zA-Z_][a-zA-Z0-9_]*)"
    r"\s*(?P<op>>=|<=|!=|==|>|<)\s*"
    r"(?P<threshold>-?[0-9]+(?:\.[0-9]+)?)$"
)


# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------


class AlertState(Enum):
    """Lifecycle states for an alert rule evaluation."""

    OK = "ok"
    PENDING = "pending"  # condition met but duration not yet elapsed
    FIRING = "firing"
    RESOLVED = "resolved"  # was firing, now ok


@dataclass
class ParsedCondition:
    """Decomposed and validated condition expression."""

    metric: str
    op_str: str
    op_fn: Callable[[float, float], bool]
    threshold: float


@dataclass
class AlertRule:
    """A single alerting rule with condition, duration, and severity."""

    name: str
    condition: str  # e.g. "backend_latency_ms > 500"
    duration_seconds: int = 0  # Seconds the condition must hold (0 = instant)
    severity: str = "warning"  # warning | critical
    channels: list[str] = field(default_factory=lambda: ["console"])

    def __post_init__(self) -> None:
        if self.severity not in ("warning", "critical"):
            raise ValueError(f"Invalid severity {self.severity!r}; expected 'warning' or 'critical'")


@dataclass
class AlertResult:
    """Outcome of evaluating a single alert rule."""

    rule_name: str
    state: AlertState
    previous_state: AlertState
    changed_at: float  # time.monotonic() timestamp
    value: float | None = None  # current metric value (None if metric missing)
    message: str = ""

    @property
    def state_changed(self) -> bool:
        """Return True if the state transitioned from the previous evaluation."""
        return self.state != self.previous_state


# ---------------------------------------------------------------------------
# Condition parser (safe -- no eval)
# ---------------------------------------------------------------------------


def parse_condition(expr: str) -> ParsedCondition:
    """Parse a condition string into its components.

    Parameters
    ----------
    expr:
        Expression like ``backend_latency_ms > 500``.

    Returns
    -------
    ParsedCondition

    Raises
    ------
    ValueError
        If the expression is malformed or uses an unsupported operator.
    """
    expr = expr.strip()
    m = _CONDITION_RE.match(expr)
    if m is None:
        raise ValueError(f"Invalid condition expression: {expr!r}")

    metric = m.group("metric")
    op_str = m.group("op")
    threshold = float(m.group("threshold"))

    op_fn = _OPERATORS.get(op_str)
    if op_fn is None:  # pragma: no cover -- regex should prevent this
        raise ValueError(f"Unsupported operator: {op_str!r}")

    return ParsedCondition(metric=metric, op_str=op_str, op_fn=op_fn, threshold=threshold)


# ---------------------------------------------------------------------------
# Internal per-rule tracking
# ---------------------------------------------------------------------------


class _RuleTracker:
    """Mutable state for a single rule's evaluation history."""

    __slots__ = ("condition_met_since", "parsed", "rule", "state")

    def __init__(self, rule: AlertRule, parsed: ParsedCondition) -> None:
        self.rule = rule
        self.parsed = parsed
        self.state: AlertState = AlertState.OK
        self.condition_met_since: float | None = None  # monotonic timestamp


# ---------------------------------------------------------------------------
# AlertEvaluator
# ---------------------------------------------------------------------------


class AlertEvaluator:
    """Evaluates alert rules against current health metrics.

    Usage::

        rules = [AlertRule(name="high-latency", condition="latency_ms > 500")]
        evaluator = AlertEvaluator(rules)
        results = evaluator.evaluate({"latency_ms": 600.0})
    """

    def __init__(self, rules: list[AlertRule]) -> None:
        self._trackers: list[_RuleTracker] = []
        for rule in rules:
            parsed = parse_condition(rule.condition)
            self._trackers.append(_RuleTracker(rule, parsed))

    # -- public API ---------------------------------------------------------

    def evaluate(self, metrics: dict[str, float]) -> list[AlertResult]:
        """Evaluate all rules against *metrics*. Returns results for **all** rules."""
        now = time.monotonic()
        results: list[AlertResult] = []
        for tracker in self._trackers:
            result = self._evaluate_one(tracker, metrics, now)
            results.append(result)
        return results

    def get_active_alerts(self) -> list[AlertResult]:
        """Return all currently firing or pending alerts."""
        now = time.monotonic()
        active: list[AlertResult] = []
        for tracker in self._trackers:
            if tracker.state in (AlertState.FIRING, AlertState.PENDING):
                active.append(
                    AlertResult(
                        rule_name=tracker.rule.name,
                        state=tracker.state,
                        previous_state=tracker.state,
                        changed_at=now,
                        message=self._format_message(tracker, tracker.state),
                    )
                )
        return active

    @property
    def rules(self) -> list[AlertRule]:
        """Return the list of configured rules."""
        return [t.rule for t in self._trackers]

    # -- internal -----------------------------------------------------------

    def _evaluate_one(
        self,
        tracker: _RuleTracker,
        metrics: dict[str, float],
        now: float,
    ) -> AlertResult:
        """Evaluate a single rule and transition state."""
        previous_state = tracker.state
        parsed = tracker.parsed
        value = metrics.get(parsed.metric)

        # If the metric is missing, treat as condition-not-met
        condition_met = False if value is None else parsed.op_fn(value, parsed.threshold)

        new_state = self._transition_met(tracker, now) if condition_met else self._transition_not_met(tracker, now)

        tracker.state = new_state

        return AlertResult(
            rule_name=tracker.rule.name,
            state=new_state,
            previous_state=previous_state,
            changed_at=now,
            value=value,
            message=self._format_message(tracker, new_state),
        )

    def _transition_met(self, tracker: _RuleTracker, now: float) -> AlertState:
        """Handle state when condition IS met."""
        rule = tracker.rule

        if tracker.state in (AlertState.OK, AlertState.RESOLVED):
            # Condition just started being true
            tracker.condition_met_since = now
            if rule.duration_seconds <= 0:
                # Instant alert -- skip PENDING
                return AlertState.FIRING
            return AlertState.PENDING

        if tracker.state == AlertState.PENDING:
            # Check if duration has elapsed
            elapsed = now - (tracker.condition_met_since or now)
            if elapsed >= rule.duration_seconds:
                return AlertState.FIRING
            return AlertState.PENDING

        # Already FIRING -- stay FIRING
        return AlertState.FIRING

    def _transition_not_met(self, tracker: _RuleTracker, now: float) -> AlertState:
        """Handle state when condition is NOT met."""
        tracker.condition_met_since = None

        if tracker.state == AlertState.FIRING:
            return AlertState.RESOLVED

        if tracker.state == AlertState.PENDING:
            # Condition cleared before duration elapsed -- back to OK
            return AlertState.OK

        if tracker.state == AlertState.RESOLVED:
            # Already resolved, return to OK
            return AlertState.OK

        # Already OK -- stay OK
        return AlertState.OK

    @staticmethod
    def _format_message(tracker: _RuleTracker, state: AlertState) -> str:
        """Generate a human-readable message for the alert result."""
        p = tracker.parsed
        if state == AlertState.FIRING:
            return f"[{tracker.rule.severity.upper()}] {tracker.rule.name}: {p.metric} {p.op_str} {p.threshold}"
        if state == AlertState.RESOLVED:
            return f"[RESOLVED] {tracker.rule.name}: condition cleared"
        if state == AlertState.PENDING:
            return f"[PENDING] {tracker.rule.name}: waiting for duration ({tracker.rule.duration_seconds}s)"
        return ""
