"""sentinel/patterns.py — PatternEngine: generalises confirmed incidents into rules.

Two-phase operation:
  Phase 1 (EXTRACT): Analyse an incident's context to derive trigger features.
  Phase 2 (GENERALISE): Create a PatternRule that fires in new contexts.

Design constraints:
  KV₃: Pattern extraction never modifies incident data.
  KV₇: Pattern matching is independent of other arms.
  KV₄: Confidence < 1.0 (capped at CONFIDENCE_CEILING).
"""

from __future__ import annotations

import logging
import re
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple

from sentinel.taxonomy import ALL_TAXONOMY, PROACTIVE_SIGNALS
from sentinel.types import (
    CONFIDENCE_CEILING,
    MAX_PATTERN_RULES,
    Incident,
    PatternRule,
)

logger = logging.getLogger(__name__)


class PatternEngine:
    """Derives generalised detection rules from confirmed incidents.

    Rule lifecycle::

        Incident → extract triggers → create / merge PatternRule (ACTIVE)
             ├─ matches → hit_count++
             ├─ false positive → false_positive_count++
             ├─ precision() < 0.3 → DEACTIVATED
             └─ 30 days no hit → PRUNED (future)
    """

    def __init__(self) -> None:
        self._rules: List[PatternRule] = []
        self._seq: int = 0

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

    def generalize(self, incident: Incident) -> Optional[PatternRule]:
        """Attempt to extract a generalisable pattern from *incident*.

        Returns ``None`` if the incident is too context-specific.
        """
        # Must be user-confirmed or high-confidence combined
        if incident.confidence < 0.7:
            return None

        triggers = self._extract_triggers(incident)
        if not triggers:
            return None

        # Check for existing rule covering same taxonomy_id
        existing = self._find_existing_rule(incident.taxonomy_id)
        if existing:
            self._merge_into_rule(existing, incident, triggers)
            return existing

        # Cap total rules
        if len(self._rules) >= MAX_PATTERN_RULES:
            # Evict the lowest-precision rule
            self._rules.sort(key=lambda r: r.precision())
            self._rules.pop(0)

        rule = PatternRule(
            id=self._next_rule_id(),
            name=f"{ALL_TAXONOMY.get(incident.taxonomy_id, 'Unknown')} Pattern",
            taxonomy_ids=[incident.taxonomy_id],
            trigger_description=incident.user_description or incident.context_snippet,
            trigger_heuristics=triggers,
            source_incident_ids=[incident.id],
        )
        self._rules.append(rule)
        return rule

    def match(
        self,
        user_msg: str,
        ai_response: str,
    ) -> Optional[Tuple[PatternRule, float]]:
        """Match current context against all active pattern rules.

        Returns ``(rule, confidence)`` for the best match, or ``None``.
        """
        best_rule: Optional[PatternRule] = None
        best_conf: float = 0.0
        combined = f"{user_msg} {ai_response}".lower()

        for rule in self._rules:
            if not rule.active:
                continue
            if rule.precision() < 0.5:
                continue  # Too many false positives

            matched = 0
            for trigger in rule.trigger_heuristics:
                try:
                    if re.search(trigger, combined, re.IGNORECASE):
                        matched += 1
                except re.error:
                    continue

            if matched == 0:
                continue

            trigger_ratio = matched / len(rule.trigger_heuristics)
            conf = min(
                0.5 * trigger_ratio * rule.precision() + rule.confidence_modifier,
                CONFIDENCE_CEILING,
            )

            if conf > best_conf:
                best_rule = rule
                best_conf = conf

        return (best_rule, best_conf) if best_rule else None

    def report_false_positive(self, rule_id: str) -> None:
        """Record a false-positive hit for the given rule.

        Auto-deactivates rules whose precision drops below 0.3.
        """
        for rule in self._rules:
            if rule.id == rule_id:
                rule.false_positive_count += 1
                if rule.precision() < 0.3:
                    rule.active = False
                return

    # ── Query ────────────────────────────────────────────────────────

    @property
    def rules(self) -> List[PatternRule]:
        return list(self._rules)

    @property
    def active_rules(self) -> List[PatternRule]:
        return [r for r in self._rules if r.active]

    def get_rule(self, rule_id: str) -> Optional[PatternRule]:
        for rule in self._rules:
            if rule.id == rule_id:
                return rule
        return None

    # ── State ────────────────────────────────────────────────────────

    def export_state(self) -> List[Dict[str, Any]]:
        return [r.to_dict() for r in self._rules]

    def import_state(self, data: List[Dict[str, Any]]) -> None:
        self._rules = []
        for raw in data:
            try:
                self._rules.append(PatternRule.from_dict(raw))
            except Exception:
                continue
        self._seq = len(self._rules)

    # ── Internal ─────────────────────────────────────────────────────

    def _next_rule_id(self) -> str:
        self._seq += 1
        ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
        return f"RULE-{ts}-{self._seq}"

    def _extract_triggers(self, incident: Incident) -> List[str]:
        """Extract regex trigger patterns from incident context.

        Strategy:
          1. Use PROACTIVE_SIGNALS as base for known taxonomy IDs.
          2. Extract significant phrases from user_description.
          3. Cross-reference context_snippet with known patterns.
          4. Deduplicate.
        """
        triggers: List[str] = []

        # Base patterns from PROACTIVE_SIGNALS
        base = PROACTIVE_SIGNALS.get(incident.taxonomy_id, [])
        triggers.extend(base)

        # User description → extract as regex-escaped phrase
        if incident.user_description:
            words = incident.user_description.lower().split()
            if len(words) >= 2:
                triggers.append(re.escape(incident.user_description.lower()))

        # Context snippet → cross-reference with known signals
        if incident.context_snippet:
            snippet_lower = incident.context_snippet.lower()
            for pattern_group in PROACTIVE_SIGNALS.values():
                for pat in pattern_group:
                    if pat.lower() in snippet_lower:
                        triggers.append(re.escape(pat))

        return list(set(triggers))

    def _find_existing_rule(self, taxonomy_id: str) -> Optional[PatternRule]:
        """Find an active rule covering *taxonomy_id*."""
        for rule in self._rules:
            if taxonomy_id in rule.taxonomy_ids and rule.active:
                return rule
        return None

    def _merge_into_rule(
        self,
        rule: PatternRule,
        incident: Incident,
        triggers: List[str],
    ) -> None:
        """Merge new triggers and incident ID into an existing rule."""
        for t in triggers:
            if t not in rule.trigger_heuristics:
                rule.trigger_heuristics.append(t)

        if incident.id not in rule.source_incident_ids:
            rule.source_incident_ids.append(incident.id)

        # Touch description if user gave a new one
        if incident.user_description and incident.user_description != rule.trigger_description:
            rule.trigger_description = incident.user_description
