"""
InsAIts SDK - Hallucination Chain Detector
===========================================
Detects when uncertain claims from previous messages are repeated
as confident assertions -- a key signal of hallucination propagation.

Core insight: Hallucinations that survive into the next message are
more dangerous than isolated ones. This detector tracks stated facts,
and flags when hedged claims from message N appear as confident
assertions in message N+1 or N+2.

Detection signals:
    1. Hedge-to-assertion promotion: "might cost $500" -> "costs $500"
    2. Numeric value drift: "$500" -> "$450" without correction
    3. Confidence escalation: hedged -> asserted across messages
"""

import re
from collections import Counter
from typing import Dict, List, Set, Tuple

# Patterns indicating uncertainty / hedging
HEDGE_PATTERNS = [
    re.compile(
        r'\b(might|may|could|possibly|perhaps|approximately|around|'
        r'roughly|estimated|about)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r"\b(I think|I believe|I'm not sure|unclear|uncertain|"
        r"allegedly|reportedly|it seems|appears to)\b",
        re.IGNORECASE,
    ),
]

# Patterns indicating confidence / assertion
ASSERTION_PATTERNS = [
    re.compile(
        r'\b(definitely|certainly|always|never|confirmed|'
        r'absolutely|undoubtedly)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(the fact that|it is known that|as established|'
        r'clearly|obviously|without doubt)\b',
        re.IGNORECASE,
    ),
]

# Pattern for numeric values (amounts, percentages, quantities)
NUMERIC_PATTERN = re.compile(
    r'\b\d+(?:\.\d+)?(?:\s*(?:%|percent|million|billion|thousand|k|M|B))?\b'
)

# Minimum keyword overlap to consider claims related
MIN_KEYWORD_OVERLAP = 2
# Minimum keyword length to consider significant
MIN_KEYWORD_LENGTH = 5


class HallucinationChainDetector:
    """Detects hallucination chain propagation across messages.

    Tracks claims with their confidence state and flags when:
    - A hedged claim in message N appears as an assertion in N+1 or N+2
    - A numeric value changes between messages without explicit correction
    - A claim is repeated with increasing confidence across the chain
    """

    name = "hallucination_chain"
    enabled = True

    def __init__(self, lookback: int = 3):
        """Initialize the detector.

        Args:
            lookback: Number of prior messages to check for promoted claims
        """
        if lookback < 1:
            raise ValueError("lookback must be at least 1")

        self.lookback = lookback
        self._claim_history: List[Dict] = []

    def check(self, text: str, message_idx: int) -> Dict:
        """Analyze a message for hallucination chain signals.

        Args:
            text: The message text to analyze
            message_idx: Sequential index of this message in the conversation

        Returns:
            Dict with detection results:
                detected (bool): Whether chain signals were found
                confidence_level (str): "hedged" or "asserted"
                promotion_flags (List[str]): Hedge-to-assertion promotions
                numeric_drift_flags (List[str]): Changed numeric values
                all_flags (List[str]): All detected signals
                description (str): Human-readable summary
        """
        hedged, asserted, confidence, numerics = self._extract_claims(text)
        numeric_flags = self._detect_numeric_drift(numerics)
        promotion_flags = self._detect_promotions(asserted)

        all_flags = list(promotion_flags) + list(numeric_flags)

        # Record this message's claims for future lookback
        self._claim_history.append({
            "idx": message_idx,
            "hedged": list(hedged),
            "asserted": list(asserted),
            "confidence": confidence,
            "numerics": list(numerics),
        })

        if all_flags:
            description = (
                f"{len(all_flags)} hallucination chain signal(s) detected."
            )
        else:
            description = "No hallucination chain patterns detected."

        return {
            "detected": len(all_flags) > 0,
            "confidence_level": confidence,
            "promotion_flags": promotion_flags,
            "numeric_drift_flags": numeric_flags,
            "all_flags": all_flags,
            "description": description,
        }

    def reset(self) -> None:
        """Reset detector state for a new conversation."""
        self._claim_history.clear()

    def _extract_claims(
        self, text: str
    ) -> Tuple[List[str], List[str], str, List[str]]:
        """Extract hedged claims, asserted claims, and numeric values.

        Returns:
            Tuple of (hedged_claims, asserted_claims, confidence_level, numerics)
        """
        sentences = re.split(r'[.!?]+', text)
        hedged_claims: List[str] = []
        asserted_claims: List[str] = []
        numerics = NUMERIC_PATTERN.findall(text)

        for sent in sentences:
            sent = sent.strip()
            if not sent or len(sent) < 10:
                continue

            hedge_score = sum(
                1 for p in HEDGE_PATTERNS if p.search(sent)
            )
            assert_score = sum(
                1 for p in ASSERTION_PATTERNS if p.search(sent)
            )

            if hedge_score > assert_score:
                hedged_claims.append(sent)
            elif assert_score > 0:
                asserted_claims.append(sent)

        if len(hedged_claims) > len(asserted_claims):
            confidence = "hedged"
        else:
            confidence = "asserted"

        return hedged_claims, asserted_claims, confidence, numerics

    def _detect_numeric_drift(self, current_nums: List[str]) -> List[str]:
        """Detect numeric values that changed from previous messages."""
        if not self._claim_history:
            return []

        prev_nums = set(self._claim_history[-1].get("numerics", []))
        curr_nums = set(current_nums)

        # Numbers present in both but different = potential drift
        novel = curr_nums - prev_nums
        dropped = prev_nums - curr_nums

        flags: List[str] = []
        if novel and dropped:
            flags.append(
                f"Numeric values changed without correction: "
                f"{dropped} replaced by {novel}"
            )
        return flags

    def _detect_promotions(self, current_asserted: List[str]) -> List[str]:
        """Detect hedged claims from prior messages now stated as assertions."""
        flags: List[str] = []

        # Check against recent prior messages
        lookback_window = self._claim_history[-self.lookback:]
        for prior in lookback_window:
            for hclaim in prior.get("hedged", []):
                h_keywords = self._extract_keywords(hclaim)
                if len(h_keywords) < MIN_KEYWORD_OVERLAP:
                    continue

                for aclaim in current_asserted:
                    a_keywords = self._extract_keywords(aclaim)
                    overlap = h_keywords & a_keywords

                    if len(overlap) >= MIN_KEYWORD_OVERLAP:
                        h_preview = hclaim[:60]
                        a_preview = aclaim[:60]
                        flags.append(
                            f"Uncertain claim promoted to assertion: "
                            f"'{h_preview}...' -> '{a_preview}...'"
                        )

        return flags

    def _extract_keywords(self, text: str) -> Set[str]:
        """Extract significant keywords from text for overlap comparison."""
        return set(
            re.findall(
                rf'\b\w{{{MIN_KEYWORD_LENGTH},}}\b',
                text.lower(),
            )
        )
