"""Notification channels for the alerting system.

Routes alert results to one or more notification channels (console logger,
webhook POST). Channels are referenced by name and matched to alert rules
via the ``channels`` list on each ``AlertRule``.
"""

from __future__ import annotations

import logging
from typing import Any, Protocol, runtime_checkable

import httpx

from llmhosts.health.alerting import AlertResult, AlertState

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Channel protocol
# ---------------------------------------------------------------------------


@runtime_checkable
class NotificationChannel(Protocol):
    """Protocol all notification channels must satisfy."""

    async def send(self, alert: AlertResult) -> bool:
        """Send a single alert notification. Return True on success."""
        ...  # pragma: no cover


# ---------------------------------------------------------------------------
# Console channel
# ---------------------------------------------------------------------------


class ConsoleChannel:
    """Logs alerts to the Python logger at appropriate severity levels."""

    async def send(self, alert: AlertResult) -> bool:
        """Log the alert using the standard logger."""
        if alert.state == AlertState.FIRING:
            if alert.rule_name and "critical" in (alert.message or "").lower():
                logger.critical("ALERT FIRING: %s | value=%s", alert.message, alert.value)
            else:
                logger.warning("ALERT FIRING: %s | value=%s", alert.message, alert.value)
        elif alert.state == AlertState.RESOLVED:
            logger.info("ALERT RESOLVED: %s", alert.message)
        elif alert.state == AlertState.PENDING:
            logger.debug("ALERT PENDING: %s", alert.message)
        return True


# ---------------------------------------------------------------------------
# Webhook channel (Slack / Discord / generic JSON POST)
# ---------------------------------------------------------------------------


class WebhookChannel:
    """Sends alerts via HTTP POST as JSON to a webhook URL.

    Compatible with generic webhook receivers, Slack incoming webhooks
    (via ``text`` field), and Discord webhooks (via ``content`` field).
    """

    def __init__(self, url: str, headers: dict[str, str] | None = None) -> None:
        self._url = url
        self._headers = headers or {}

    async def send(self, alert: AlertResult) -> bool:
        """POST alert as JSON to the configured webhook URL.

        Returns True on success (2xx), False otherwise.
        """
        payload: dict[str, Any] = {
            "rule_name": alert.rule_name,
            "state": alert.state.value,
            "previous_state": alert.previous_state.value,
            "value": alert.value,
            "message": alert.message,
            # Slack-compatible
            "text": alert.message,
            # Discord-compatible
            "content": alert.message,
        }

        try:
            async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
                resp = await client.post(
                    self._url,
                    json=payload,
                    headers={"Content-Type": "application/json", **self._headers},
                )
                if resp.is_success:
                    logger.debug("Webhook notification sent to %s (status %d)", self._url, resp.status_code)
                    return True
                logger.warning(
                    "Webhook notification failed: %s returned %d",
                    self._url,
                    resp.status_code,
                )
                return False
        except httpx.HTTPError as exc:
            logger.error("Webhook notification error for %s: %s", self._url, exc)
            return False


# ---------------------------------------------------------------------------
# Notification router
# ---------------------------------------------------------------------------


class NotificationRouter:
    """Routes alert results to the appropriate notification channels.

    Maps channel names (e.g. ``"console"``, ``"slack"``) to channel
    implementations and dispatches notifications for alerts whose state
    has changed.
    """

    def __init__(self, channels: dict[str, NotificationChannel]) -> None:
        self._channels = channels

    async def notify(self, alerts: list[AlertResult]) -> None:
        """Send notifications for alerts that have changed state.

        Only alerts where ``state_changed`` is True are dispatched.
        Each alert is sent to every channel listed in its originating
        rule's ``channels`` configuration. Since ``AlertResult`` does not
        carry the full rule, callers should pass a ``rule_channels``
        mapping or use :meth:`notify_with_channels` instead.
        """
        for alert in alerts:
            if not alert.state_changed:
                continue
            # Skip OK state -- only notify on PENDING, FIRING, RESOLVED
            if alert.state == AlertState.OK:
                continue
            await self._dispatch(alert, list(self._channels.keys()))

    async def notify_with_channels(
        self,
        alerts: list[AlertResult],
        rule_channels: dict[str, list[str]],
    ) -> None:
        """Send notifications, routing each alert to its rule's channels.

        Parameters
        ----------
        alerts:
            List of alert evaluation results.
        rule_channels:
            Mapping of ``rule_name -> list[channel_name]``.
        """
        for alert in alerts:
            if not alert.state_changed:
                continue
            if alert.state == AlertState.OK:
                continue
            channels = rule_channels.get(alert.rule_name, list(self._channels.keys()))
            await self._dispatch(alert, channels)

    async def _dispatch(self, alert: AlertResult, channel_names: list[str]) -> None:
        """Send an alert to the named channels."""
        for name in channel_names:
            channel = self._channels.get(name)
            if channel is None:
                logger.warning("Unknown notification channel %r for alert %s", name, alert.rule_name)
                continue
            try:
                await channel.send(alert)
            except Exception as exc:
                logger.error(
                    "Failed to send alert %s to channel %s: %s",
                    alert.rule_name,
                    name,
                    exc,
                )
