"""
InsAIts SDK - Jargon Drift Detector
=====================================
Detects when agents develop or use terminology that was not
established in the conversation -- a sign of cross-LLM jargon
or gradual semantic drift in vocabulary.

Uses out-of-vocabulary (OOV) tracking:
    - Builds a running vocabulary from all seen messages
    - Flags terms that appear for the first time at high frequency
    - Tracks acronyms and compound terms that appear without prior definition
    - Applies a grace period to avoid flagging the first few messages
"""

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


# Patterns for detecting acronyms (2-6 uppercase letters)
ACRONYM_PATTERN = re.compile(r'\b[A-Z]{2,6}\b')

# Patterns for detecting compound terms (hyphenated words)
COMPOUND_PATTERN = re.compile(r'\b\w+-\w+(?:-\w+)?\b')

# Patterns for explicit term definitions in text
_DEFINITION_PATTERNS = [
    re.compile(r'(\w+)\s+(?:is|means|refers to|stands for|defined as)', re.I),
    re.compile(r"(?:we call|let's call|henceforth)\s+(\w+)", re.I),
    re.compile(r'([A-Z]{2,6})\s+\('),  # "API (Application Programming Interface)"
]

# Grace period: don't flag until this many messages have been seen
GRACE_PERIOD_ACRONYMS = 2
GRACE_PERIOD_NOVEL = 3

# Minimum times a novel term must appear in a single message to be flagged
MIN_NOVEL_TERM_FREQUENCY = 2

# Maximum novel terms to report (prevents noise)
MAX_NOVEL_TERMS_REPORTED = 10

# Minimum token length to track in vocabulary
MIN_TOKEN_LENGTH = 4


class JargonDriftDetector:
    """Detects jargon and vocabulary drift in agent communications.

    Maintains a running vocabulary from all messages and flags:
    - Undefined acronyms appearing after the grace period
    - Novel high-frequency terms not seen in prior messages
    - Sudden vocabulary shifts indicating cross-LLM jargon leakage
    """

    name = "jargon_drift"
    enabled = True

    def __init__(self, oov_threshold: float = 0.3):
        """Initialize the jargon drift detector.

        Args:
            oov_threshold: Fraction of novel tokens to trigger detection
                (0.3 = flag if 30% of tokens are new, after grace period)
        """
        self.oov_threshold = oov_threshold
        self._vocabulary: Counter = Counter()
        self._defined_terms: Set[str] = set()
        self._message_count: int = 0

    def check(self, text: str) -> Dict:
        """Analyze a message for jargon drift signals.

        Args:
            text: The message text to analyze

        Returns:
            Dict with detection results:
                detected (bool): Whether jargon signals were found
                undefined_acronyms (List[str]): Undefined acronyms found
                novel_terms (List[str]): Novel high-frequency terms
                vocabulary_size (int): Current vocabulary size
                flags (List[str]): All detected signals
                description (str): Human-readable summary
        """
        self._message_count += 1
        tokens = self._tokenize(text)
        defined_here = self._extract_definitions(text)
        self._defined_terms.update(defined_here)

        # Find undefined acronyms
        undefined_acronyms = self._find_undefined_acronyms(text)

        # Find novel high-frequency terms
        novel_terms = self._find_novel_terms(tokens)

        # Update running vocabulary AFTER detection
        self._vocabulary.update(tokens)

        flags: List[str] = []
        if undefined_acronyms:
            unique_acronyms = sorted(set(undefined_acronyms))
            flags.append(
                f"Undefined acronyms: {', '.join(unique_acronyms)}"
            )
        if novel_terms:
            flags.append(
                f"Novel repeated terms not in prior vocabulary: "
                f"{', '.join(novel_terms[:MAX_NOVEL_TERMS_REPORTED])}"
            )

        if flags:
            description = (
                f"{len(flags)} jargon signal(s): {'; '.join(flags)}"
            )
        else:
            description = "Vocabulary consistent with prior messages."

        return {
            "detected": len(flags) > 0,
            "undefined_acronyms": sorted(set(undefined_acronyms)),
            "novel_terms": novel_terms[:MAX_NOVEL_TERMS_REPORTED],
            "vocabulary_size": len(self._vocabulary),
            "flags": flags,
            "description": description,
        }

    def reset(self) -> None:
        """Reset detector state for a new conversation."""
        self._vocabulary.clear()
        self._defined_terms.clear()
        self._message_count = 0

    def _tokenize(self, text: str) -> List[str]:
        """Extract significant tokens from text."""
        return re.findall(
            rf'\b[a-zA-Z]\w{{{MIN_TOKEN_LENGTH - 1},}}\b',
            text.lower(),
        )

    def _extract_definitions(self, text: str) -> Set[str]:
        """Extract terms that are explicitly defined in this message."""
        defined: Set[str] = set()
        for pattern in _DEFINITION_PATTERNS:
            for match in pattern.finditer(text):
                defined.add(match.group(1).lower())
        return defined

    def _find_undefined_acronyms(self, text: str) -> List[str]:
        """Find acronyms not previously defined or seen in vocabulary."""
        if self._message_count <= GRACE_PERIOD_ACRONYMS:
            return []

        acronyms = ACRONYM_PATTERN.findall(text)
        undefined: List[str] = []
        for acronym in acronyms:
            lower = acronym.lower()
            if (
                lower not in self._defined_terms
                and lower not in self._vocabulary
                and acronym.lower() not in self._vocabulary
            ):
                undefined.append(acronym)

        return undefined

    def _find_novel_terms(self, tokens: List[str]) -> List[str]:
        """Find tokens that appear frequently but are not in vocabulary."""
        if self._message_count <= GRACE_PERIOD_NOVEL:
            return []

        token_freq = Counter(tokens)
        novel: List[str] = [
            term
            for term, count in token_freq.most_common()
            if count >= MIN_NOVEL_TERM_FREQUENCY
            and self._vocabulary.get(term, 0) == 0
        ]
        return novel
