"""
AlertEngine — evaluates alert rules against audit events and fires notifications.

Rule types:
  denial_spike        — X or more denials within the last N minutes
  new_source          — a source_id not seen in any prior audit event for this tenant
  isolation_violation — a session is touched by 2+ distinct agent_roles
  high_deny_rate      — deny% >= threshold in the last N minutes

Delivery channels (per rule):
  webhook_url  — POST JSON to the URL (works with Slack, PagerDuty, Teams, etc.)
  email        — SMTP; requires SMTP_HOST env var (SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM optional)

Rule evaluation is synchronous so deliveries are always recorded before the
evaluate response returns. Only the actual HTTP/SMTP call is dispatched on a
daemon thread so slow webhooks never stall the API.
"""

import json
import os
import threading
from datetime import datetime, timedelta

from autopil.models import AuditEvent, Decision


class AlertEngine:

    def __init__(self, alert_store, audit_log):
        self.store = alert_store
        self.audit = audit_log

    def check(self, event: AuditEvent, tenant_id: str) -> None:
        """
        Evaluate all enabled rules for this tenant against the current event.
        Rule evaluation and delivery-record writes are synchronous.
        Actual HTTP/SMTP dispatch is async so webhooks never block the response.
        """
        rules = self.store.list_rules(tenant_id)
        now = datetime.utcnow()
        for rule in rules:
            if not rule["is_enabled"]:
                continue
            if self._in_cooldown(rule, now):
                continue
            detail = self._evaluate_rule(rule, event, tenant_id, now)
            if detail is not None:
                self._fire(rule, detail, tenant_id, now)

    # ── internal ──────────────────────────────────────────────────────────────

    @staticmethod
    def _to_naive_utc(dt) -> datetime:
        """Normalise a datetime to offset-naive UTC so comparisons always work."""
        if dt is None:
            return None
        if isinstance(dt, str):
            dt = datetime.fromisoformat(dt)
        if dt.tzinfo is not None:
            from datetime import timezone
            dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
        return dt

    def _in_cooldown(self, rule: dict, now: datetime) -> bool:
        last = rule.get("last_fired_at")
        if not last:
            return False
        last_dt = self._to_naive_utc(last)
        return (now - last_dt).total_seconds() < rule["cooldown_minutes"] * 60

    def _evaluate_rule(self, rule: dict, event: AuditEvent, tenant_id: str, now: datetime) -> dict | None:
        events = self.audit.get_all_events(tenant_id)
        window_start = now - timedelta(minutes=rule["window_minutes"])
        recent = [e for e in events if self._to_naive_utc(e.timestamp) >= window_start]
        rule_type = rule["rule_type"]

        if rule_type == "denial_spike":
            denials = [e for e in recent if e.decision == Decision.DENY]
            if len(denials) >= rule["threshold"]:
                return {
                    "denials_in_window": len(denials),
                    "threshold": rule["threshold"],
                    "window_minutes": rule["window_minutes"],
                    "triggering_event_id": event.event_id,
                }

        elif rule_type == "new_source":
            prior_sources = {e.source_id for e in events if e.event_id != event.event_id}
            if event.source_id not in prior_sources:
                return {
                    "new_source": event.source_id,
                    "first_seen": event.timestamp.isoformat(),
                    "agent_role": event.agent_role,
                }

        elif rule_type == "isolation_violation":
            session_roles = {e.agent_role for e in events if e.session_id == event.session_id}
            if len(session_roles) >= 2:
                return {
                    "session_id": event.session_id,
                    "agent_roles": sorted(session_roles),
                    "event_count": len([e for e in events if e.session_id == event.session_id]),
                }

        elif rule_type == "high_deny_rate":
            if recent:
                deny_count = sum(1 for e in recent if e.decision == Decision.DENY)
                rate = (deny_count / len(recent)) * 100
                if rate >= rule["threshold"]:
                    return {
                        "deny_rate_pct": round(rate, 1),
                        "threshold_pct": rule["threshold"],
                        "events_in_window": len(recent),
                        "denials": deny_count,
                    }

        return None

    def _fire(self, rule: dict, detail: dict, tenant_id: str, now: datetime) -> None:
        """
        Record the delivery synchronously, then dispatch the notification async.
        This ensures delivery history is always up-to-date regardless of webhook latency.
        """
        self.store.mark_fired(rule["rule_id"], now.isoformat())

        payload = {
            "alert":     rule["name"],
            "rule_id":   rule["rule_id"],
            "rule_type": rule["rule_type"],
            "tenant_id": tenant_id,
            "fired_at":  now.isoformat() + "Z",
            "detail":    detail,
        }

        has_webhook = bool(rule.get("webhook_url"))
        has_email   = bool(rule.get("email"))

        if not has_webhook and not has_email:
            # no delivery channel — record as no_destination and return
            self.store.record_delivery({
                "rule_id":   rule["rule_id"],
                "tenant_id": tenant_id,
                "fired_at":  now.isoformat(),
                "rule_name": rule["name"],
                "rule_type": rule["rule_type"],
                "detail":    json.dumps(detail),
                "status":    "no_destination",
                "error":     None,
            })
            return

        # record as queued immediately so history is visible before delivery completes
        self.store.record_delivery({
            "rule_id":   rule["rule_id"],
            "tenant_id": tenant_id,
            "fired_at":  now.isoformat(),
            "rule_name": rule["name"],
            "rule_type": rule["rule_type"],
            "detail":    json.dumps(detail),
            "status":    "queued",
            "error":     None,
        })

        # dispatch actual HTTP/SMTP call on a daemon thread
        t = threading.Thread(
            target=self._send_notifications,
            args=(rule, payload),
            daemon=True,
        )
        t.start()

    def _send_notifications(self, rule: dict, payload: dict) -> None:
        """Fire-and-forget: send to webhook and/or email. Errors are logged but not re-raised."""
        if rule.get("webhook_url"):
            try:
                import urllib.request
                data = json.dumps(payload).encode()
                req = urllib.request.Request(
                    rule["webhook_url"], data=data,
                    headers={"Content-Type": "application/json"}, method="POST",
                )
                urllib.request.urlopen(req, timeout=10)
            except Exception:
                pass  # delivery failure is non-fatal

        if rule.get("email"):
            try:
                self._send_email(rule["email"], rule["name"], payload)
            except Exception:
                pass

    def _send_email(self, to_addr: str, subject: str, payload: dict) -> None:
        import smtplib
        from email.message import EmailMessage

        host      = os.environ.get("SMTP_HOST", "localhost")
        port      = int(os.environ.get("SMTP_PORT", "587"))
        user      = os.environ.get("SMTP_USER", "")
        password  = os.environ.get("SMTP_PASS", "")
        from_addr = os.environ.get("SMTP_FROM", user or "autopil@localhost")

        msg = EmailMessage()
        msg["Subject"] = f"[AutoPIL Alert] {subject}"
        msg["From"]    = from_addr
        msg["To"]      = to_addr
        msg.set_content(json.dumps(payload, indent=2))

        with smtplib.SMTP(host, port) as smtp:
            if user:
                smtp.starttls()
                smtp.login(user, password)
            smtp.send_message(msg)
