"""sentinel/types.py — Data types for the Epistemic Sentinel.

Core types for metacognitive failure detection, classification,
and pattern learning during AI–user conversation.

Design constraints:
  KV₃:  All types are inert data — no I/O, no side-effects.
  KV₄:  Confidence ∈ (0, CONFIDENCE_CEILING) — never certainty.
  KV₇:  Types are independent of lens/session state.
  AX57: Every incident carries full traceability.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Dict, List, Optional


# ── Hard limits (from EPISTEMIC_SENTINEL_PLAN §2.2) ─────────────────

CONFIDENCE_CEILING: float = 0.90
MAX_REGISTRY_ENTRIES: int = 500
MAX_PATTERN_RULES: int = 100
MAX_PICKER_TOKENS: int = 50
MAX_SENTINEL_OVERHEAD: int = 150
NOISE_FLOOR: float = 0.25


class DetectionArm(Enum):
    """Which arm detected the incident."""
    REACTIVE = "reactive"        # User explicitly flagged
    PROACTIVE = "proactive"      # Heuristic detected
    STRUCTURAL = "structural"    # Pattern match from registry


class Severity(Enum):
    """Incident severity.  Mirrors gate verdicts."""
    INFO = "info"           # Logged, no action
    WARNING = "warning"     # Flag to user/LLM
    CRITICAL = "critical"   # Immediate intervention


class ConfidenceBasis(Enum):
    """What grounds the confidence value."""
    USER_CONFIRMED = "user_confirmed"   # User classified via picker
    HEURISTIC = "heuristic"             # Mechanical proxy detected
    PATTERN_MATCH = "pattern_match"     # Registry match
    COMBINED = "combined"               # Multiple arms agree


@dataclass
class SentinelSignal:
    """A single detection signal emitted by any arm.

    Lightweight: ≤ 8 fields.  This is the wire format that flows
    between Arms → Registry → Pattern Engine → Co-Articulation.
    """
    taxonomy_id: str                        # "CD-5", "FM-2", "FM-11", etc.
    arm: DetectionArm = DetectionArm.PROACTIVE
    confidence: float = 0.5                 # ∈ (0, CONFIDENCE_CEILING)
    basis: ConfidenceBasis = ConfidenceBasis.HEURISTIC
    severity: Severity = Severity.INFO
    context_snippet: str = ""               # ≤ 200 chars of relevant context
    suggested_label: str = ""               # Co-articulation's best description
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __post_init__(self):
        # Clamp confidence to ceiling (KV₄)
        if self.confidence >= CONFIDENCE_CEILING:
            self.confidence = CONFIDENCE_CEILING
        if self.confidence < 0.0:
            self.confidence = 0.01

    def to_dict(self) -> Dict[str, Any]:
        d = asdict(self)
        d["arm"] = self.arm.value
        d["severity"] = self.severity.value
        d["basis"] = self.basis.value
        return d

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "SentinelSignal":
        """Reconstruct from a serialized dict."""
        data = dict(data)  # Don't mutate caller's dict
        if "arm" in data and isinstance(data["arm"], str):
            data["arm"] = DetectionArm(data["arm"])
        if "severity" in data and isinstance(data["severity"], str):
            data["severity"] = Severity(data["severity"])
        if "basis" in data and isinstance(data["basis"], str):
            data["basis"] = ConfidenceBasis(data["basis"])
        return cls(**{k: v for k, v in data.items()
                      if k in cls.__dataclass_fields__})


@dataclass
class Incident:
    """A confirmed or high-confidence detection, persisted to the registry.

    Created when:
      - User classifies via picker (reactive, confidence ≥ 0.8)
      - Two+ arms agree on the same taxonomy_id (combined, confidence = avg)
      - Structural arm matches with confidence ≥ 0.7
    """
    id: str                                 # "INC-{timestamp}-{seq}"
    taxonomy_id: str                        # Primary classification
    taxonomy_ids: List[str] = field(default_factory=list)  # Co-occurring IDs
    arm: DetectionArm = DetectionArm.REACTIVE
    confidence: float = 0.5
    basis: ConfidenceBasis = ConfidenceBasis.HEURISTIC
    severity: Severity = Severity.WARNING
    context_snippet: str = ""
    user_description: str = ""              # Free-text from picker (optional)
    resolution: str = ""                    # How it was resolved (post-hoc)
    session_turn: int = 0                   # Turn number when detected
    timestamp: str = ""                     # ISO 8601
    generalized: bool = False               # Pattern engine processed this?
    pattern_rule_id: Optional[str] = None   # If generalized, which rule?

    def __post_init__(self):
        if not self.timestamp:
            self.timestamp = datetime.now(timezone.utc).isoformat()
        if self.taxonomy_id not in self.taxonomy_ids:
            self.taxonomy_ids = [self.taxonomy_id] + [
                t for t in self.taxonomy_ids if t != self.taxonomy_id
            ]
        # Clamp confidence (KV₄)
        if self.confidence >= CONFIDENCE_CEILING:
            self.confidence = CONFIDENCE_CEILING
        if self.confidence < 0.0:
            self.confidence = 0.01

    def to_dict(self) -> Dict[str, Any]:
        d = asdict(self)
        d["arm"] = self.arm.value
        d["severity"] = self.severity.value
        d["basis"] = self.basis.value
        return d

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "Incident":
        """Reconstruct from a serialized dict."""
        data = dict(data)
        if "arm" in data and isinstance(data["arm"], str):
            data["arm"] = DetectionArm(data["arm"])
        if "severity" in data and isinstance(data["severity"], str):
            data["severity"] = Severity(data["severity"])
        if "basis" in data and isinstance(data["basis"], str):
            data["basis"] = ConfidenceBasis(data["basis"])
        return cls(**{k: v for k, v in data.items()
                      if k in cls.__dataclass_fields__})


@dataclass
class PatternRule:
    """A generalized detection rule derived from confirmed incidents.

    "If the pattern is made explicit in one place, it applies to others."
    Implements cross-context transfer.
    """
    id: str                                 # "PAT-{seq}"
    name: str                               # Human-readable pattern name
    taxonomy_ids: List[str] = field(default_factory=list)
    trigger_description: str = ""           # What to look for
    trigger_heuristics: List[str] = field(default_factory=list)  # Regex/keyword triggers
    source_incident_ids: List[str] = field(default_factory=list)
    hit_count: int = 0                      # How many times this rule matched
    false_positive_count: int = 0           # User-reported false positives
    confidence_modifier: float = 0.0        # Adjust base confidence: +/-
    created: str = ""
    last_hit: str = ""
    active: bool = True                     # Can be deactivated if noisy

    def __post_init__(self):
        if not self.created:
            self.created = datetime.now(timezone.utc).isoformat()

    def precision(self) -> float:
        """Estimated precision = hits / (hits + false_positives)."""
        total = self.hit_count + self.false_positive_count
        return self.hit_count / total if total > 0 else 0.5

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "PatternRule":
        """Reconstruct from a serialized dict."""
        return cls(**{k: v for k, v in data.items()
                      if k in cls.__dataclass_fields__})


@dataclass
class CoSuggestion:
    """A vocabulary suggestion from the Co-Articulation module.

    Over time, the system builds better descriptions of failure modes
    from accumulated user descriptions + context snippets.
    """
    taxonomy_id: str
    suggested_phrases: List[str] = field(default_factory=list)
    usage_count: int = 0                    # How often this suggestion was shown
    acceptance_count: int = 0               # How often user accepted/modified it
    last_updated: str = ""

    def acceptance_rate(self) -> float:
        return (self.acceptance_count / self.usage_count
                if self.usage_count > 0 else 0.0)

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "CoSuggestion":
        """Reconstruct from a serialized dict."""
        return cls(**{k: v for k, v in data.items()
                      if k in cls.__dataclass_fields__})


@dataclass
class SentinelState:
    """Complete serializable state for persistence."""
    schema_version: int = 1
    incidents: List[Dict[str, Any]] = field(default_factory=list)
    pattern_rules: List[Dict[str, Any]] = field(default_factory=list)
    co_suggestions: List[Dict[str, Any]] = field(default_factory=list)
    session_stats: Dict[str, Any] = field(default_factory=dict)
    saved_at: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)
