"""
cortex/aegis/categorizer.py — Sentry error categorization engine.

Pure deterministic logic. No LLM calls. No imports beyond stdlib.
Runs in <1ms per event.

11 categories:
  deploy_regression  — error first seen within 30min of last deploy
  config_error       — missing env var, invalid config
  provider_failure   — external API down/erroring
  rate_limit         — 429 / quota exceeded
  timeout            — TimeoutError variants
  auth_failure       — 401/403, AuthenticationError
  memory_error       — embedding/memory pipeline failures
  storage_error      — SQLite/DB access failures
  data_integrity     — corrupt/unexpected data shape
  fleet_error        — fleet/inter-agent communication
  unknown            — fallback
"""
from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import List, Optional

# ── Error class sets ──────────────────────────────────────────────────────────

_CONFIG_CLASSES = frozenset({
    "KeyError", "ValueError", "ConfigurationError", "SettingsError",
    "ImproperlyConfigured", "EnvVarNotSet", "MissingEnvironmentVariable",
    "AttributeError",
})
_CONFIG_MESSAGE_PATTERNS = [
    r"\benv\b", r"\bconfig\b", r"\bsetting\b",
    r"API_KEY", r"SECRET", r"DSN", r"TOKEN",
    r"environment variable", r"OPENAI_API_KEY",
]

_PROVIDER_CLASSES = frozenset({
    "APIError", "OpenAIError", "VoyageError", "AnthropicError",
    "ElevenLabsError", "HTTPStatusError", "ServiceUnavailableError",
    "HTTPError", "ConnectionError", "RemoteDisconnected",
})
_PROVIDER_CULPRIT_PATTERNS = [
    r"\bopenai\b", r"\banthropic\b", r"\bvoyage\b",
    r"\believenlabs\b", r"\bhttpx\b", r"\baiohttp\b",
    r"\brequests\b", r"api\.client", r"external",
]

_RATE_LIMIT_CLASSES = frozenset({
    "RateLimitError", "TooManyRequestsError", "QuotaExceededError",
    "RateLimitExceededError",
})
_RATE_LIMIT_PATTERNS = [
    r"rate.?limit", r"too many requests", r"quota.?exceeded",
    r"\b429\b", r"throttl",
]

_TIMEOUT_CLASSES = frozenset({
    "TimeoutError", "ReadTimeout", "ConnectTimeout", "WriteTimeout",
    "asyncio.TimeoutError", "httpx.TimeoutException", "socket.timeout",
    "TimeoutException",
})
_TIMEOUT_PATTERNS = [r"timed? out", r"\btimeout\b", r"deadline exceeded"]

_DATA_INTEGRITY_CLASSES = frozenset({
    "ValidationError", "JSONDecodeError",
    "IntegrityError", "DataError", "ParseError", "DeserializationError",
    "pydantic.ValidationError", "AssertionError",
})
_DATA_INTEGRITY_CULPRIT = [r"\bparse\b", r"\bvalidat\b", r"\bjson\b", r"\bschema\b"]

_AUTH_CLASSES = frozenset({
    "AuthenticationError", "PermissionError", "Unauthorized",
    "ForbiddenError", "InvalidToken", "ExpiredToken",
})
_AUTH_PATTERNS = [r"\bauth\b", r"\bunauthorized\b", r"\bforbidden\b", r"\b401\b", r"\b403\b"]

_MEMORY_CULPRIT = [r"\bmemory\b", r"\bembed\b", r"\bvector\b", r"\brecall\b"]
_MEMORY_CLASSES = frozenset({"EmbeddingError", "RecallError", "MemoryError"})

_STORAGE_CLASSES = frozenset({
    "OperationalError", "DatabaseError", "InterfaceError",
    "sqlite3.Error", "SQLAlchemyError", "ProgrammingError",
})
_STORAGE_CULPRIT = [r"\bstorage\b", r"\bsqlite\b", r"\.db\b", r"\bdatabase\b"]

_FLEET_CULPRIT = [r"\bfleet\b", r"\bannounce\b", r"\bdispatch\b", r"\bhmac\b"]
_FLEET_CLASSES = frozenset({"FleetError", "DispatchError", "AnnounceError"})

# Classes that always trigger critical severity
_CRITICAL_CLASSES = frozenset({
    "MemoryError", "SystemExit", "KeyboardInterrupt",
    "RecursionError", "StackOverflowError",
    "EmbeddingError",  # embedding pipeline critical
})


# ── Data classes ──────────────────────────────────────────────────────────────

@dataclass
class SentryEvent:
    """Normalized Sentry event fields for categorization."""
    error_class: str           # e.g. "KeyError"
    error_message: str         # e.g. "'content'"
    culprit: str               # e.g. "cortex.core.memory in store_memory"
    endpoint: str              # e.g. "/api/v1/remember"
    sentry_level: str          # fatal|error|warning|info
    substatus: str             # new|escalating|regressed|ongoing|...
    first_seen: datetime
    last_seen: datetime
    count: int
    issue_id: str
    short_id: str
    sentry_url: str
    release: str               # e.g. "v1.5.2"
    environment: str           # e.g. "production"
    stack_frames: List[dict] = field(default_factory=list)


@dataclass
class CategorizedAlert:
    """Result of categorization."""
    category: str
    severity: str              # critical|warning|info
    error_class: str
    summary: str
    endpoint: str
    count_1h: int
    first_seen: str            # ISO 8601
    sentry_url: str
    suggested_playbook: str
    deploy_correlated: bool
    source: str = "aegis-sentry"


# ── Helpers ───────────────────────────────────────────────────────────────────

def _matches_any(text: str, patterns: list) -> bool:
    """Case-insensitive match against any regex pattern."""
    text_lower = text.lower()
    for p in patterns:
        if re.search(p, text_lower):
            return True
    return False


# ── Public API ────────────────────────────────────────────────────────────────

def categorize(
    event: SentryEvent,
    last_deploy_time: Optional[datetime] = None,
) -> CategorizedAlert:
    """
    Deterministic error categorization.
    Returns a CategorizedAlert with category, severity, and playbook.
    """
    ec = event.error_class
    msg = event.error_message
    culprit = event.culprit

    # ── Deploy correlation ───────────────────────────────────────────────────
    deploy_correlated = False
    if (
        last_deploy_time is not None
        and event.substatus == "new"
        and event.first_seen >= last_deploy_time
        and (event.first_seen - last_deploy_time) <= timedelta(minutes=30)
    ):
        deploy_correlated = True

    # ── Category detection (priority order) ──────────────────────────────────
    category = _determine_category(ec, msg, culprit, deploy_correlated)

    # ── Severity ─────────────────────────────────────────────────────────────
    severity = _determine_severity(event, deploy_correlated)

    # ── Playbook + summary ───────────────────────────────────────────────────
    playbook = _suggest_playbook(category)
    summary = _build_summary(event)

    return CategorizedAlert(
        category=category,
        severity=severity,
        error_class=ec,
        summary=summary,
        endpoint=event.endpoint,
        count_1h=event.count,
        first_seen=event.first_seen.isoformat(),
        sentry_url=event.sentry_url,
        suggested_playbook=playbook,
        deploy_correlated=deploy_correlated,
    )


def _determine_category(
    ec: str,
    msg: str,
    culprit: str,
    deploy_correlated: bool,
) -> str:
    """Priority-ordered category detection."""
    combined = f"{ec} {msg} {culprit}"

    # deploy_regression supersedes everything
    if deploy_correlated:
        return "deploy_regression"

    # Rate limit (before provider — it's a subtype)
    if ec in _RATE_LIMIT_CLASSES or _matches_any(combined, _RATE_LIMIT_PATTERNS):
        return "rate_limit"

    # Timeout
    if ec in _TIMEOUT_CLASSES or _matches_any(combined, _TIMEOUT_PATTERNS):
        return "timeout"

    # Auth
    if ec in _AUTH_CLASSES or _matches_any(combined, _AUTH_PATTERNS):
        return "auth_failure"

    # Provider failure (external API)
    if ec in _PROVIDER_CLASSES or _matches_any(culprit, _PROVIDER_CULPRIT_PATTERNS):
        return "provider_failure"

    # Config error (must match both class AND message patterns to avoid FP)
    if ec in _CONFIG_CLASSES and _matches_any(combined, _CONFIG_MESSAGE_PATTERNS):
        return "config_error"

    # Memory/embedding pipeline
    if ec in _MEMORY_CLASSES or _matches_any(culprit, _MEMORY_CULPRIT):
        return "memory_error"

    # Storage/DB
    if ec in _STORAGE_CLASSES or _matches_any(culprit, _STORAGE_CULPRIT):
        return "storage_error"

    # Data integrity
    if ec in _DATA_INTEGRITY_CLASSES or _matches_any(culprit, _DATA_INTEGRITY_CULPRIT):
        return "data_integrity"

    # Fleet
    if ec in _FLEET_CLASSES or _matches_any(culprit, _FLEET_CULPRIT):
        return "fleet_error"

    return "unknown"


def _determine_severity(event: SentryEvent, deploy_correlated: bool) -> str:
    """
    Severity rules:
    - fatal Sentry level → always critical
    - deploy regression → critical
    - new issue (first time ever seen) → critical
    - error class in critical_classes → critical
    - escalating or regressed → warning
    - high count (>50) → warning
    - single occurrence → info
    - Sentry warning level → info
    - default → warning
    """
    if event.sentry_level == "fatal":
        return "critical"
    if deploy_correlated:
        return "critical"
    if event.substatus == "new":
        return "critical"
    if event.error_class in _CRITICAL_CLASSES:
        return "critical"
    if event.substatus in ("escalating", "regressed"):
        return "warning"
    if event.count > 50:
        return "warning"
    if event.count == 1:
        return "info"
    if event.sentry_level == "warning":
        return "info"
    return "warning"


def _suggest_playbook(category: str) -> str:
    """Map category to a playbook name."""
    _playbooks = {
        "deploy_regression": "check_recent_deploy",
        "config_error": "check_env_vars",
        "provider_failure": "check_provider_status",
        "rate_limit": "check_rate_limits_and_throttle",
        "timeout": "check_provider_latency",
        "auth_failure": "rotate_or_verify_credentials",
        "memory_error": "check_embedding_pipeline",
        "storage_error": "check_db_health",
        "data_integrity": "check_input_validation",
        "fleet_error": "check_fleet_connectivity",
        "unknown": "investigate_manually",
    }
    return _playbooks.get(category, "investigate_manually")


def _build_summary(event: SentryEvent) -> str:
    """Build a human-readable one-line summary."""
    frame = next(
        (f for f in reversed(event.stack_frames) if f.get("in_app")),
        None,
    )
    if frame:
        location = f"{frame.get('filename', '?')}:{frame.get('lineno', '?')}"
        return f"{event.error_class} in {location}: {event.error_message[:100]}"
    return f"{event.error_class}: {event.error_message[:120]}"
