"""
Fast Detector — Tier 1 synchronous detection (<5ms).

Runs in the hot path on every LLM/tool response. Zero network calls,
zero LLM calls. Pure CPU: keyword matching, hash lookups, counter checks.

Designed for 100s of millions of agent executions at minimal overhead.
"""

import hashlib
import logging
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List

logger = logging.getLogger("aigie.runtime.fast_detector")


class DetectionSeverity(str, Enum):
    """Severity of a detected issue."""

    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class DetectionCategory(str, Enum):
    """Category of detected issue."""

    ERROR = "error"
    DRIFT = "drift"
    ANOMALY = "anomaly"
    LOOP = "loop"
    BUDGET = "budget"
    PATTERN_MATCH = "pattern_match"
    QUALITY = "quality"


@dataclass(slots=True)
class Detection:
    """A single detection result. Uses __slots__ for zero-alloc-overhead."""

    category: DetectionCategory
    severity: DetectionSeverity
    error_type: str
    description: str
    confidence: float = 1.0
    suggested_action: str = ""  # RETRY, FALLBACK, TRUNCATE, SKIP, BLOCK
    action_params: Dict[str, Any] = field(default_factory=dict)
    cached_fix: Dict[str, Any] | None = None  # From pattern cache
    needs_deep_eval: bool = False  # Flag for Tier 2 async judge


@dataclass(slots=True)
class FastDetectionResult:
    """Aggregated result from fast detection pass."""

    detections: List[Detection] = field(default_factory=list)
    signal_hints: List[str] = field(default_factory=list)  # Lightweight tags for JudgeSelector
    elapsed_us: float = 0.0  # Microseconds
    has_critical: bool = False
    has_cached_fix: bool = False
    needs_deep_eval: bool = False

    @property
    def has_issues(self) -> bool:
        return len(self.detections) > 0


# Pre-compiled keyword tuples for O(1)-ish matching (tuple iteration is C-speed).
_RATE_LIMIT_KW = ("rate limit", "429", "too many requests", "quota exceeded", "throttl")
_TIMEOUT_KW = ("timeout", "timed out", "deadline exceeded", "request timed out")
_CONNECTION_KW = (
    "connection refused",
    "econnrefused",
    "econnreset",
    "network error",
    "unavailable",
)
_AUTH_KW = ("unauthorized", "401", "403", "forbidden", "invalid api key", "authentication")
_CONTEXT_KW = (
    "context length",
    "maximum context",
    "token limit",
    "too many tokens",
    "context window",
)
_CONTENT_FILTER_KW = ("content filter", "content_filter", "safety system", "content policy")
_MODEL_KW = ("model not found", "model_not_found", "deprecated", "does not exist")
_JSON_KW = ("json", "parse error", "invalid json", "unexpected token", "decode error")

# Map keyword groups → (error_type, severity, action)
_ERROR_RULES = [
    (
        _RATE_LIMIT_KW,
        "RATE_LIMIT",
        DetectionSeverity.HIGH,
        "RETRY",
        {"backoff_base": 1.0, "max_retries": 3},
    ),
    (
        _TIMEOUT_KW,
        "TIMEOUT",
        DetectionSeverity.HIGH,
        "RETRY",
        {"backoff_base": 0.5, "max_retries": 2},
    ),
    (
        _CONNECTION_KW,
        "CONNECTION_ERROR",
        DetectionSeverity.HIGH,
        "RETRY",
        {"backoff_base": 1.0, "max_retries": 2},
    ),
    (_AUTH_KW, "AUTH_ERROR", DetectionSeverity.CRITICAL, "ALERT", {}),
    (_CONTEXT_KW, "CONTEXT_OVERFLOW", DetectionSeverity.HIGH, "TRUNCATE", {"reduction_pct": 0.3}),
    (_CONTENT_FILTER_KW, "CONTENT_FILTER", DetectionSeverity.MEDIUM, "REPHRASE", {}),
    (_MODEL_KW, "MODEL_UNAVAILABLE", DetectionSeverity.HIGH, "FALLBACK", {}),
    (
        _JSON_KW,
        "JSON_PARSE_ERROR",
        DetectionSeverity.MEDIUM,
        "RETRY",
        {"add_instruction": "respond in valid JSON"},
    ),
]


# ── Quality heuristic keyword tuples (Tier 0/1 response quality checks) ──

# Refusal detection — model refuses to answer
_REFUSAL_KW = (
    "i cannot",
    "i can't",
    "i'm unable to",
    "i am unable to",
    "i'm not able to",
    "i am not able to",
    "i must decline",
    "i'm sorry, but i can't",
    "as an ai, i cannot",
    "i don't have the ability",
    "i do not have the ability",
    "it would be inappropriate for me",
)

# Safety/guardrail activation — model's safety system kicked in
_SAFETY_KW = (
    "violates my guidelines",
    "against my programming",
    "i'm designed to be helpful",
    "i need to be careful",
    "i should not provide",
    "i cannot assist with",
    "this request goes against",
    "i'm not comfortable",
    "potentially harmful",
    "could be dangerous",
)

# Hallucination markers — fabricated references, over-confident assertions
_HALLUCINATION_KW = (
    "according to the study published in",
    "research conducted in 2024 shows",
    "research conducted in 2025 shows",
    "a study by dr.",
    "published in the journal of",
    "doi:10.",
    "isbn:",
    "et al. (20",  # Fabricated academic citations
)

# Lazy/deflection responses — model avoids doing the work
_LAZY_KW = (
    "as mentioned earlier",
    "as i said before",
    "i already explained",
    "please refer to my previous",
    "as previously stated",
    "the rest of the implementation is left as an exercise",
    "// ... rest of the code",
    "# ... remaining implementation",
    "... and so on",
    "etc. etc.",
)


class FastDetector:
    """Tier 1 fast detector — runs synchronously in <5ms.

    All methods are designed for minimal allocation and zero I/O.
    Thread-safe for read path (pattern cache updated atomically via dict swap).
    """

    def __init__(self) -> None:
        # Pattern cache: error_signature → cached fix (swapped atomically)
        self._pattern_cache: Dict[str, Dict[str, Any]] = {}

        # Loop detection: (trace_id, tool_name) → call count
        self._tool_call_counts: Dict[str, int] = {}

        # Output history for repetition detection: trace_id → list of output hashes
        self._output_hashes: Dict[str, List[str]] = {}

        # Budget tracking: trace_id → {tokens, cost}
        self._budgets: Dict[str, Dict[str, float]] = {}

        # Platform-pushed custom rules: list of {keywords: [...], error_type, severity, action, params}
        self._custom_rules: List[Dict[str, Any]] = []

        # Config
        self.max_tool_calls_per_trace = 20
        self.max_repeated_outputs = 3
        self.token_budget = 0  # 0 = unlimited
        self.cost_budget = 0.0  # 0 = unlimited
        self.enable_quality_checks = True  # Tier 0 quality heuristics
        self.min_response_ratio = 0.02  # Min output_len/input_len to flag as off-topic

    def detect(
        self,
        *,
        trace_id: str,
        span_id: str,
        output_content: str | None,
        error: str | None = None,
        error_type_name: str | None = None,
        status_code: int | None = None,
        tool_name: str | None = None,
        model: str | None = None,
        input_tokens: int = 0,
        output_tokens: int = 0,
        cost: float = 0.0,
        duration_ms: float = 0.0,
        metadata: Dict[str, Any] | None = None,
    ) -> FastDetectionResult:
        """Run all Tier 0/1 checks. Target: <5ms total, typically <1ms."""
        start = time.monotonic()
        detections: List[Detection] = []

        # 1. Error classification (keyword matching)
        if error:
            det = self._classify_error(error, status_code)
            if det:
                detections.append(det)

        # 2. Cached pattern matching
        if error:
            cached = self._check_pattern_cache(error, error_type_name)
            if cached:
                detections.append(cached)

        # 3. Output anomalies (skip when error is present — empty response is expected)
        if not error:
            anomaly = self._check_output_anomaly(output_content, duration_ms)
            if anomaly:
                detections.append(anomaly)

        # 4. Loop detection
        if tool_name:
            loop = self._check_loop(trace_id, tool_name)
            if loop:
                detections.append(loop)

        # 5. Repetition detection
        if output_content:
            rep = self._check_repetition(trace_id, output_content)
            if rep:
                detections.append(rep)

        # 6. Budget checks
        budget = self._check_budget(trace_id, input_tokens + output_tokens, cost)
        if budget:
            detections.append(budget)

        # 7. Quality signal hints (Tier 0 — keyword-based, feeds JudgeSelector)
        # These don't create Detection objects — they produce lightweight tags
        # that tell the JudgeSelector which platform judges to prioritize.
        signal_hints: List[str] = []
        if self.enable_quality_checks and output_content and not error:
            signal_hints = self._produce_signal_hints(
                output_content,
                input_tokens,
                output_tokens,
            )

        # 8. Platform-pushed custom rules
        if output_content and self._custom_rules:
            custom_dets = self._check_custom_rules(output_content, error)
            detections.extend(custom_dets)

        elapsed_us = (time.monotonic() - start) * 1_000_000
        has_critical = any(d.severity == DetectionSeverity.CRITICAL for d in detections)
        has_cached = any(d.cached_fix is not None for d in detections)

        # needs_deep_eval: escalate to Tier 2 when signals suggest it or when
        # there's content but no high-confidence local resolution.
        resolved_high_confidence = any(
            d.confidence >= 0.85 and d.suggested_action and not d.needs_deep_eval
            for d in detections
        )
        explicitly_needs_deep = any(d.needs_deep_eval for d in detections)
        # Signal hints always mean "the platform judges should look at this"
        signals_suggest_eval = bool(signal_hints)
        needs_deep = (
            explicitly_needs_deep
            or signals_suggest_eval
            or (
                not detections
                and output_content
                and len(output_content) > 20
                and not resolved_high_confidence
            )
        )

        return FastDetectionResult(
            detections=detections,
            signal_hints=signal_hints,
            elapsed_us=elapsed_us,
            has_critical=has_critical,
            has_cached_fix=has_cached,
            needs_deep_eval=needs_deep,
        )

    # ------------------------------------------------------------------
    # Detection methods (all designed for <1ms each)
    # ------------------------------------------------------------------

    def _classify_error(self, error: str, status_code: int | None) -> Detection | None:
        """Classify error by keyword matching. ~50μs."""
        err_lower = error.lower()
        for keywords, err_type, severity, action, params in _ERROR_RULES:
            if any(kw in err_lower for kw in keywords):
                return Detection(
                    category=DetectionCategory.ERROR,
                    severity=severity,
                    error_type=err_type,
                    description=f"Detected {err_type}: {error[:120]}",
                    confidence=0.95,
                    suggested_action=action,
                    action_params=dict(params),
                )
        # Check HTTP status code directly
        if status_code:
            if status_code == 429:
                return Detection(
                    category=DetectionCategory.ERROR,
                    severity=DetectionSeverity.HIGH,
                    error_type="RATE_LIMIT",
                    description="HTTP 429 rate limit",
                    confidence=0.99,
                    suggested_action="RETRY",
                    action_params={"backoff_base": 1.0, "max_retries": 3},
                )
            if status_code >= 500:
                return Detection(
                    category=DetectionCategory.ERROR,
                    severity=DetectionSeverity.HIGH,
                    error_type="SERVER_ERROR",
                    description=f"HTTP {status_code} server error",
                    confidence=0.90,
                    suggested_action="RETRY",
                    action_params={"backoff_base": 0.5, "max_retries": 2},
                )
        return None

    def _check_pattern_cache(self, error: str, error_type_name: str | None) -> Detection | None:
        """Check against cached patterns from platform. O(1) dict lookup."""
        if not self._pattern_cache:
            return None

        # Try exact error type first, then signature hash
        key = error_type_name or ""
        cached = self._pattern_cache.get(key)
        if not cached:
            # Hash first 200 chars as signature
            sig = hashlib.md5(error[:200].lower().encode(), usedforsecurity=False).hexdigest()[:16]
            cached = self._pattern_cache.get(sig)

        if cached:
            return Detection(
                category=DetectionCategory.PATTERN_MATCH,
                severity=DetectionSeverity.HIGH,
                error_type=cached.get("error_type", "CACHED_PATTERN"),
                description=f"Matched cached pattern: {cached.get('name', 'unknown')}",
                confidence=cached.get("confidence", 0.8),
                suggested_action=cached.get("fix_action", "RETRY"),
                action_params=cached.get("fix_params", {}),
                cached_fix=cached,
            )
        return None

    def _check_output_anomaly(self, output: str | None, duration_ms: float) -> Detection | None:
        """Check for empty/truncated/abnormal output. ~10μs."""
        if output is None or output == "":
            return Detection(
                category=DetectionCategory.ANOMALY,
                severity=DetectionSeverity.MEDIUM,
                error_type="EMPTY_RESPONSE",
                description="LLM returned empty/null response",
                confidence=0.85,
                suggested_action="RETRY",
                action_params={"add_instruction": "Please provide a complete response."},
                needs_deep_eval=True,
            )
        if len(output) < 5 and duration_ms > 100:
            return Detection(
                category=DetectionCategory.ANOMALY,
                severity=DetectionSeverity.LOW,
                error_type="TRUNCATED_RESPONSE",
                description=f"Suspiciously short response ({len(output)} chars after {duration_ms:.0f}ms)",
                confidence=0.5,
                needs_deep_eval=True,
            )
        return None

    def _check_loop(self, trace_id: str, tool_name: str) -> Detection | None:
        """Detect tool call loops. O(1) counter increment."""
        key = f"{trace_id}:{tool_name}"
        count = self._tool_call_counts.get(key, 0) + 1
        self._tool_call_counts[key] = count

        if count > self.max_tool_calls_per_trace:
            return Detection(
                category=DetectionCategory.LOOP,
                severity=DetectionSeverity.HIGH,
                error_type="TOOL_LOOP",
                description=f"Tool '{tool_name}' called {count} times (limit: {self.max_tool_calls_per_trace})",
                confidence=0.90,
                suggested_action="BREAK_LOOP",
                action_params={"tool_name": tool_name, "call_count": count},
            )
        return None

    def _check_repetition(self, trace_id: str, output: str) -> Detection | None:
        """Detect repetitive outputs via hash comparison. ~20μs."""
        h = hashlib.md5(output.encode(), usedforsecurity=False).hexdigest()[:16]
        history = self._output_hashes.setdefault(trace_id, [])

        # Count how many recent outputs match
        recent = history[-10:]  # Check last 10
        matches = sum(1 for prev in recent if prev == h)
        history.append(h)

        # Cap history length
        if len(history) > 50:
            self._output_hashes[trace_id] = history[-50:]

        if matches >= self.max_repeated_outputs:
            return Detection(
                category=DetectionCategory.LOOP,
                severity=DetectionSeverity.MEDIUM,
                error_type="REPETITIVE_OUTPUT",
                description=f"Same output repeated {matches + 1} times",
                confidence=0.80,
                suggested_action="INJECT_DIVERSITY",
                action_params={"instruction": "Provide a different approach or perspective."},
                needs_deep_eval=True,
            )
        return None

    def _check_budget(self, trace_id: str, tokens: int, cost: float) -> Detection | None:
        """Check token/cost budget. O(1)."""
        budget = self._budgets.setdefault(trace_id, {"tokens": 0, "cost": 0.0})
        budget["tokens"] += tokens
        budget["cost"] += cost

        if self.token_budget > 0 and budget["tokens"] > self.token_budget:
            return Detection(
                category=DetectionCategory.BUDGET,
                severity=DetectionSeverity.CRITICAL,
                error_type="TOKEN_BUDGET_EXCEEDED",
                description=f"Token budget exceeded: {budget['tokens']}/{self.token_budget}",
                confidence=1.0,
                suggested_action="BLOCK",
            )
        if self.cost_budget > 0 and budget["cost"] > self.cost_budget:
            return Detection(
                category=DetectionCategory.BUDGET,
                severity=DetectionSeverity.CRITICAL,
                error_type="COST_BUDGET_EXCEEDED",
                description=f"Cost budget exceeded: ${budget['cost']:.4f}/${self.cost_budget:.4f}",
                confidence=1.0,
                suggested_action="BLOCK",
            )
        return None

    # ------------------------------------------------------------------
    # Signal hint production (lightweight tags for JudgeSelector)
    # ------------------------------------------------------------------

    def _produce_signal_hints(
        self,
        output: str,
        input_tokens: int,
        output_tokens: int,
    ) -> List[str]:
        """Produce signal hints from response content. ~100μs total.

        Signal hints are lightweight string tags that tell the JudgeSelector
        which platform judges to prioritize. They do NOT suggest actions —
        the platform's predefined judges decide what to do.

        Returns list of signal tags like: ["refusal", "hallucination_marker", "lazy"]
        """
        hints: List[str] = []
        out_lower = output.lower()

        # Refusal signal
        if any(kw in out_lower for kw in _REFUSAL_KW):
            hints.append("refusal")

        # Safety/guardrail activation signal
        if any(kw in out_lower for kw in _SAFETY_KW):
            hints.append("safety_triggered")

        # Hallucination markers signal
        if any(kw in out_lower for kw in _HALLUCINATION_KW):
            hints.append("hallucination_marker")

        # Lazy/deflection signal
        if any(kw in out_lower for kw in _LAZY_KW):
            hints.append("lazy_response")

        # Off-topic signal: very short output relative to input
        if input_tokens > 100 and output_tokens > 0:
            ratio = output_tokens / input_tokens
            if ratio < self.min_response_ratio:
                hints.append("suspiciously_short")

        return hints

    def _check_custom_rules(
        self,
        output: str | None,
        error: str | None,
    ) -> List[Detection]:
        """Check platform-pushed custom detection rules. ~100μs per rule."""
        results: List[Detection] = []
        text = (output or "").lower() + " " + (error or "").lower()

        for rule in self._custom_rules:
            keywords = rule.get("keywords", ())
            if any(kw in text for kw in keywords):
                results.append(
                    Detection(
                        category=DetectionCategory(rule.get("category", "quality")),
                        severity=DetectionSeverity(rule.get("severity", "medium")),
                        error_type=rule.get("error_type", "CUSTOM_RULE"),
                        description=rule.get("description", "Matched custom platform rule"),
                        confidence=rule.get("confidence", 0.75),
                        suggested_action=rule.get("action", ""),
                        action_params=rule.get("action_params", {}),
                        needs_deep_eval=rule.get("needs_deep_eval", False),
                    )
                )
        return results

    # ------------------------------------------------------------------
    # Pattern cache management
    # ------------------------------------------------------------------

    def update_pattern_cache(self, patterns: Dict[str, Dict[str, Any]]) -> None:
        """Atomically swap the pattern cache (thread-safe via dict assignment)."""
        self._pattern_cache = patterns

    def update_custom_rules(self, rules: List[Dict[str, Any]]) -> None:
        """Atomically swap custom detection rules from platform.

        Each rule: {keywords: [str], error_type: str, severity: str,
                    category: str, action: str, action_params: dict,
                    confidence: float, description: str, needs_deep_eval: bool}
        """
        # Pre-lowercase keywords for O(1)-ish matching at detect time
        processed = []
        for rule in rules:
            r = dict(rule)
            r["keywords"] = tuple(kw.lower() for kw in r.get("keywords", []))
            processed.append(r)
        self._custom_rules = processed

    def clear_trace(self, trace_id: str) -> None:
        """Clean up per-trace state. Call when trace completes."""
        # Remove all keys starting with trace_id
        keys_to_remove = [k for k in self._tool_call_counts if k.startswith(trace_id)]
        for k in keys_to_remove:
            del self._tool_call_counts[k]
        self._output_hashes.pop(trace_id, None)
        self._budgets.pop(trace_id, None)

    def get_stats(self) -> Dict[str, Any]:
        """Get detector stats."""
        return {
            "cached_patterns": len(self._pattern_cache),
            "custom_rules": len(self._custom_rules),
            "quality_checks_enabled": self.enable_quality_checks,
            "active_traces": len(self._budgets),
            "tool_call_counters": len(self._tool_call_counts),
        }
