"""
InsAIts SDK - Uncertainty Propagation Detector
===============================================
Tracks uncertainty markers (partial, truncated, estimated, unknown)
as they flow through a multi-agent pipeline. Flags when downstream
agents silently drop or contradict uncertainty from upstream.

Core insight: Tools and agents often return results with uncertainty
flags ("partial results", "approximately 50", "truncated at 1000 rows").
Downstream agents tend to silently promote these to certainties
("the complete list shows...", "there are exactly 50..."). This is
a data integrity failure that no SQL validator can catch.

Detection signals:
    1. Certainty promotion: upstream says "partial" -> downstream says "complete"
    2. Qualifier stripping: "approximately 50" -> "50" (exact, no qualifier)
    3. Uncertainty contradiction: upstream "unknown" -> downstream "confirmed"
    4. Missing propagation: upstream flags uncertainty, downstream ignores it entirely
"""

import re
from typing import Dict, List, Optional, Set, Tuple

# Markers indicating the data or claim is uncertain/incomplete
UNCERTAINTY_MARKERS = [
    re.compile(
        r'\b(partial|truncated|incomplete|limited|estimated|'
        r'approximate|approximately|roughly|about|around|'
        r'uncertain|unclear|unverified|unconfirmed)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(might be|may be|could be|possibly|perhaps|'
        r'not sure|not certain|not confirmed|not verified)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(up to|at most|at least|no more than|fewer than|'
        r'more than|over|under|between .+ and)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(sample|subset|preview|first \d+|top \d+|'
        r'limited to|capped at|max \d+)\b',
        re.IGNORECASE,
    ),
]

# Markers indicating certainty/completeness -- contradicts upstream uncertainty
CERTAINTY_MARKERS = [
    re.compile(
        r'\b(complete|comprehensive|exhaustive|full|total|'
        r'entire|all|every|definitive|confirmed|verified)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(exactly|precisely|always|never|certainly|'
        r'definitely|absolutely|undoubtedly|without doubt)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(the complete list|all results|every record|'
        r'the full dataset|total count is|there are exactly)\b',
        re.IGNORECASE,
    ),
]

# Common data-context keywords to match claims across messages
DATA_KEYWORDS_PATTERN = re.compile(
    r'\b(users?|records?|rows?|results?|items?|entries?|'
    r'count|total|sum|average|data|table|query|report|'
    r'list|set|collection|dataset|response|output)\b',
    re.IGNORECASE,
)

# Minimum overlap to consider two messages about the same topic
MIN_TOPIC_OVERLAP = 2
# Minimum keyword length for topic matching
MIN_KEYWORD_LENGTH = 4


class UncertaintyPropagationDetector:
    """Detects when uncertainty markers are dropped or contradicted
    by downstream agents in a multi-agent pipeline.

    Tracks uncertainty state across messages and flags:
    - Certainty promotions (uncertain -> certain without new evidence)
    - Qualifier stripping (approximate value -> exact value)
    - Missing uncertainty propagation (upstream uncertain, downstream ignores)
    """

    name = "uncertainty_propagation"
    enabled = True

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

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

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

    def check(self, text: str, message_idx: int) -> Dict:
        """Analyze a message for uncertainty propagation failures.

        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 propagation failures were found
                uncertainty_markers_found (List[str]): Uncertainty in current msg
                certainty_markers_found (List[str]): Certainty in current msg
                upstream_uncertain (bool): Whether recent upstream was uncertain
                flags (List[str]): All detected propagation failures
                description (str): Human-readable summary
        """
        curr_uncertainty = self._find_markers(text, UNCERTAINTY_MARKERS)
        curr_certainty = self._find_markers(text, CERTAINTY_MARKERS)
        curr_topics = self._extract_data_topics(text)

        flags: List[str] = []

        # Check against recent upstream messages
        upstream_uncertain = False
        recent = self._message_history[-self.lookback:]

        for prior in recent:
            if not prior["has_uncertainty"]:
                continue

            # Check if current message is about the same topic
            topic_overlap = curr_topics & prior["topics"]
            if len(topic_overlap) < MIN_TOPIC_OVERLAP:
                continue

            upstream_uncertain = True
            shared = ", ".join(sorted(topic_overlap)[:3])

            # Signal 1: Certainty promotion
            if curr_certainty and not curr_uncertainty:
                for marker in curr_certainty:
                    flags.append(
                        f"Certainty promotion: upstream had uncertainty "
                        f"about [{shared}], downstream states '{marker}' "
                        f"without new evidence"
                    )

            # Signal 2: Upstream uncertain, downstream makes no mention
            if not curr_uncertainty and not curr_certainty:
                if len(topic_overlap) >= MIN_TOPIC_OVERLAP + 1:
                    flags.append(
                        f"Uncertainty dropped: upstream flagged uncertainty "
                        f"about [{shared}], downstream ignores it entirely"
                    )

        # Signal 3: Direct contradiction within same message
        contradictions = self._find_internal_contradictions(text)
        flags.extend(contradictions)

        # Record this message
        self._message_history.append({
            "idx": message_idx,
            "has_uncertainty": len(curr_uncertainty) > 0,
            "has_certainty": len(curr_certainty) > 0,
            "uncertainty_markers": curr_uncertainty,
            "certainty_markers": curr_certainty,
            "topics": curr_topics,
        })

        if flags:
            description = (
                f"{len(flags)} uncertainty propagation failure(s) detected. "
                f"Downstream agent may be presenting uncertain data as definitive."
            )
        else:
            description = "No uncertainty propagation issues detected."

        return {
            "detected": len(flags) > 0,
            "uncertainty_markers_found": curr_uncertainty,
            "certainty_markers_found": curr_certainty,
            "upstream_uncertain": upstream_uncertain,
            "flags": flags,
            "description": description,
        }

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

    def _find_markers(
        self, text: str, patterns: List[re.Pattern]
    ) -> List[str]:
        """Find all matching markers in text, return unique matched strings."""
        found: List[str] = []
        for pattern in patterns:
            matches = pattern.findall(text)
            found.extend(matches)
        # Deduplicate while preserving order
        seen: Set[str] = set()
        result: List[str] = []
        for m in found:
            lower = m.lower()
            if lower not in seen:
                seen.add(lower)
                result.append(m)
        return result

    def _extract_data_topics(self, text: str) -> Set[str]:
        """Extract data-related topic keywords for cross-message matching."""
        matches = DATA_KEYWORDS_PATTERN.findall(text.lower())
        # Also grab longer content words
        words = re.findall(
            rf'\b\w{{{MIN_KEYWORD_LENGTH},}}\b', text.lower()
        )
        stopwords = frozenset({
            'that', 'this', 'with', 'from', 'have', 'been', 'were',
            'will', 'would', 'could', 'should', 'into', 'about',
            'also', 'just', 'more', 'some', 'than', 'then', 'them',
            'they', 'what', 'when', 'which', 'your', 'does', 'each',
        })
        content_words = [w for w in words if w not in stopwords]
        return set(matches + content_words[:20])

    def _find_internal_contradictions(self, text: str) -> List[str]:
        """Find sentences that contain both uncertainty and certainty markers
        about the same claim -- internal inconsistency."""
        sentences = re.split(r'[.!?\n]+', text)
        flags: List[str] = []

        for sent in sentences:
            sent = sent.strip()
            if len(sent) < 20:
                continue

            has_uncertain = any(p.search(sent) for p in UNCERTAINTY_MARKERS)
            has_certain = any(p.search(sent) for p in CERTAINTY_MARKERS)

            if has_uncertain and has_certain:
                preview = sent[:80]
                flags.append(
                    f"Internal contradiction: sentence contains both "
                    f"uncertainty and certainty markers: '{preview}...'"
                )

        return flags
