"""Streaming / incremental detection mode.

Flags critical issues (5xx, SQL errors, timeouts, Redis/gRPC errors)
as they're recorded, before the full batch analysis pass.
Useful for long-running sessions where immediate feedback is valuable.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Optional


class AlertLevel(Enum):
    INFO = "info"
    WARNING = "warning"
    CRITICAL = "critical"


@dataclass
class StreamingAlert:
    """An alert emitted in real-time during recording."""
    level: AlertLevel
    category: str
    message: str
    record_seq: int = 0
    evidence: Dict[str, Any] = field(default_factory=dict)


class StreamingDetector:
    """Lightweight detector that processes records one at a time.

    Call `on_record()` for each captured I/O operation.
    Subscribe to alerts via `on_alert` callback.
    """

    def __init__(
        self,
        on_alert: Optional[Callable[[StreamingAlert], None]] = None,
        http_error_threshold: int = 500,
        slow_http_ms: float = 10000,
        slow_sql_ms: float = 5000,
        slow_redis_ms: float = 1000,
    ):
        self.on_alert = on_alert
        self.http_error_threshold = http_error_threshold
        self.slow_http_ms = slow_http_ms
        self.slow_sql_ms = slow_sql_ms
        self.slow_redis_ms = slow_redis_ms
        self.alerts: List[StreamingAlert] = []
        self._alert_count_by_category: Dict[str, int] = {}
        self._seen_alert_keys: Dict[str, int] = {}  # dedup key -> count
        self._max_dedup_keys = 5000

    def on_record(self, record: Any) -> Optional[StreamingAlert]:
        """Process a single record and emit alert if critical.

        Returns the alert if one was generated, else None.
        """
        alert = None

        if hasattr(record, "status") and hasattr(record, "url_canonical"):
            alert = self._check_http(record)
        elif hasattr(record, "sql_canonical") and hasattr(record, "exception"):
            alert = self._check_sql(record)
        elif hasattr(record, "command") and hasattr(record, "error"):
            alert = self._check_redis(record)
        elif hasattr(record, "status_code") and hasattr(record, "service"):
            alert = self._check_grpc(record)

        if alert:
            self._emit(alert)

        return alert

    def _check_http(self, record: Any) -> Optional[StreamingAlert]:
        status = record.status
        if status is None:
            return None
        url = str(getattr(record, "url_canonical", "") or "")
        duration = getattr(record, "duration_ms", 0) or 0

        if status >= self.http_error_threshold:
            return StreamingAlert(
                level=AlertLevel.CRITICAL,
                category="http_5xx",
                message=f"HTTP {status} from {url}",
                record_seq=getattr(record, "seq", 0),
                evidence={"status": status, "url": url},
            )

        if duration > self.slow_http_ms:
            return StreamingAlert(
                level=AlertLevel.WARNING,
                category="slow_http",
                message=f"Slow HTTP ({duration:.0f}ms) to {url}",
                record_seq=getattr(record, "seq", 0),
                evidence={"duration_ms": duration, "url": url},
            )

        return None

    def _check_sql(self, record: Any) -> Optional[StreamingAlert]:
        sql_canonical = str(getattr(record, "sql_canonical", "") or "")
        if record.exception:
            msg = (
                record.exception.get("message", "SQL error")
                if isinstance(record.exception, dict)
                else str(record.exception)
            )
            msg = str(msg or "SQL error")
            return StreamingAlert(
                level=AlertLevel.CRITICAL,
                category="sql_error",
                message=f"SQL error: {msg[:200]}",
                record_seq=getattr(record, "seq", 0),
                evidence={"query": sql_canonical[:100], "error": msg[:200]},
            )

        duration = getattr(record, "duration_ms", 0) or 0
        if duration > self.slow_sql_ms:
            return StreamingAlert(
                level=AlertLevel.WARNING,
                category="slow_sql",
                message=f"Slow SQL ({duration:.0f}ms)",
                record_seq=getattr(record, "seq", 0),
                evidence={"duration_ms": duration, "query": sql_canonical[:100]},
            )

        return None

    def _check_redis(self, record: Any) -> Optional[StreamingAlert]:
        command = str(getattr(record, "command", "UNKNOWN") or "UNKNOWN")
        error = getattr(record, "error", None)
        if error:
            error_text = str(error)
            return StreamingAlert(
                level=AlertLevel.CRITICAL,
                category="redis_error",
                message=f"Redis {command} error: {error_text[:200]}",
                record_seq=getattr(record, "seq", 0),
                evidence={"command": command, "error": error_text[:200]},
            )

        duration = getattr(record, "duration_ms", 0) or 0
        if duration > self.slow_redis_ms:
            return StreamingAlert(
                level=AlertLevel.WARNING,
                category="slow_redis",
                message=f"Slow Redis {command} ({duration:.0f}ms)",
                record_seq=getattr(record, "seq", 0),
                evidence={"command": command, "duration_ms": duration},
            )

        return None

    def _check_grpc(self, record: Any) -> Optional[StreamingAlert]:
        if record.status_code in (13, 14, 4):  # INTERNAL, UNAVAILABLE, DEADLINE_EXCEEDED
            service = str(getattr(record, "service", "") or "")
            method = str(getattr(record, "method", "unknown") or "unknown")
            svc = f"{service}/{method}" if service else method
            return StreamingAlert(
                level=AlertLevel.CRITICAL,
                category="grpc_error",
                message=f"gRPC error (status {record.status_code}) from {svc}",
                record_seq=getattr(record, "seq", 0),
                evidence={
                    "status_code": record.status_code,
                    "service": service,
                    "method": method,
                },
            )
        return None

    def _emit(self, alert: StreamingAlert) -> None:
        # Deduplicate on stable serialized evidence to avoid hash instability/collisions.
        try:
            evidence_fingerprint = json.dumps(alert.evidence or {}, sort_keys=True, default=str)[:1024]
        except Exception:
            evidence_fingerprint = str(alert.evidence)[:1024]
        dedup_key = f"{alert.category}:{evidence_fingerprint}"

        self._alert_count_by_category[alert.category] = (
            self._alert_count_by_category.get(alert.category, 0) + 1
        )

        if dedup_key in self._seen_alert_keys:
            self._seen_alert_keys[dedup_key] += 1
            return  # Suppress duplicate

        self._seen_alert_keys[dedup_key] = 1
        if len(self._seen_alert_keys) > self._max_dedup_keys:
            # Keep memory bounded for long-running streams.
            self._seen_alert_keys.pop(next(iter(self._seen_alert_keys)))
        self.alerts.append(alert)
        if self.on_alert:
            self.on_alert(alert)

    @property
    def deduplicated_count(self) -> int:
        """Number of alerts suppressed by deduplication."""
        return sum(v - 1 for v in self._seen_alert_keys.values() if v > 1)

    @property
    def critical_count(self) -> int:
        return sum(1 for a in self.alerts if a.level == AlertLevel.CRITICAL)

    @property
    def warning_count(self) -> int:
        return sum(1 for a in self.alerts if a.level == AlertLevel.WARNING)

    def summary(self) -> str:
        if not self.alerts:
            return "No issues detected during recording."
        return (
            f"Streaming detection: {self.critical_count} critical, "
            f"{self.warning_count} warnings across {len(self.alerts)} alerts"
        )
