"""
InsAIts SDK - Query-Intent Divergence Detector
===============================================
Compares a user's natural language intent against the actual query or action
generated by an agent. Detects when the agent's output diverges from what
was requested.

Example failure modes:
    - User asks "show sales by region" -> agent queries by product category
    - User asks "average revenue last 30 days" -> agent computes sum, no date filter
    - User asks "top 5 customers" -> agent returns all customers unsorted

Detection approach (no NLP library required):
    1. Extract key entities (nouns, proper nouns) via simple heuristics
    2. Match aggregation words (sum, avg, count, max, min)
    3. Detect filter/scope patterns (date ranges, limits, exclusions)
    4. Optional: embedding-based cosine similarity for semantic distance
"""

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

# Words that indicate aggregation intent
AGGREGATION_WORDS = {
    "sum", "total", "average", "avg", "mean", "median",
    "count", "number", "min", "minimum", "max", "maximum",
    "percentage", "percent", "ratio", "rate",
}

# SQL-like aggregation functions (appear in generated queries)
SQL_AGGREGATION_PATTERN = re.compile(
    r'\b(SUM|AVG|COUNT|MIN|MAX|MEAN|MEDIAN|GROUP\s+BY)\b',
    re.IGNORECASE,
)

# Date/time filter patterns in natural language
DATE_FILTER_PATTERNS = [
    re.compile(
        r'\b(last|past|previous|recent)\s+\d+\s+'
        r'(days?|weeks?|months?|years?|hours?|minutes?)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(since|after|before|between|from|until|through)\s+'
        r'(\d{4}[-/]\d{1,2}[-/]\d{1,2}|\w+\s+\d{1,2}|'
        r'yesterday|today|last\s+\w+)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(this|current)\s+(week|month|quarter|year|day)\b',
        re.IGNORECASE,
    ),
]

# Scope/limit patterns
SCOPE_PATTERNS = [
    re.compile(
        r'\b(top|first|bottom|last|lowest|highest)\s+\d+\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(limit|only|just|exactly|at most|no more than)\s+\d+\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\bLIMIT\s+\d+\b',
        re.IGNORECASE,
    ),
]

# Exclusion/filter patterns
EXCLUSION_PATTERNS = [
    re.compile(
        r'\b(exclude|excluding|except|without|not including|'
        r'ignore|ignoring|omit|omitting|remove|removing)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\b(only|solely|exclusively|specifically|just)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\bWHERE\b',
        re.IGNORECASE,
    ),
]

# Sort/order patterns
SORT_PATTERNS = [
    re.compile(
        r'\b(sort|sorted|order|ordered|rank|ranked|ascending|'
        r'descending|asc|desc|highest|lowest|biggest|smallest)\b',
        re.IGNORECASE,
    ),
    re.compile(
        r'\bORDER\s+BY\b',
        re.IGNORECASE,
    ),
]

# Stopwords for entity extraction
_STOPWORDS = frozenset({
    "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
    "have", "has", "had", "do", "does", "did", "will", "would", "could",
    "should", "may", "might", "can", "shall", "must", "need", "ought",
    "to", "of", "in", "for", "on", "at", "by", "with", "from", "as",
    "into", "through", "during", "before", "after", "above", "below",
    "between", "out", "off", "up", "down", "over", "under", "again",
    "further", "then", "once", "here", "there", "when", "where", "why",
    "how", "all", "each", "every", "both", "few", "more", "most", "other",
    "some", "such", "no", "nor", "not", "only", "own", "same", "so",
    "than", "too", "very", "just", "about", "also", "and", "but", "or",
    "if", "that", "this", "what", "which", "who", "whom", "its", "it",
    "me", "my", "we", "our", "you", "your", "he", "she", "they", "them",
    "his", "her", "their", "i", "show", "get", "give", "find", "tell",
    "make", "list", "display", "return", "provide", "see", "look",
    "select", "from", "where", "group", "order", "having", "join",
    "inner", "outer", "left", "right", "cross", "union", "insert",
    "update", "delete", "create", "drop", "alter", "index", "table",
    "view", "into", "values", "set", "null", "true", "false",
})

MIN_ENTITY_LENGTH = 3


class QueryIntentDetector:
    """Detects divergence between user intent and generated query/action.

    Compares entities, aggregations, filters, scope, and sort directives
    between a natural-language intent and the resulting query or action text.
    Optionally uses embeddings for semantic similarity.
    """

    name = "query_intent"
    enabled = True

    def __init__(
        self,
        similarity_threshold: float = 0.65,
        embed_fn: Optional[Callable] = None,
    ):
        """Initialize the detector.

        Args:
            similarity_threshold: Cosine similarity below this triggers a flag.
                Only used when embed_fn is provided.
            embed_fn: Optional function(text) -> np.ndarray for embeddings.
                If None, only keyword-based checks are used.
        """
        if not 0.0 < similarity_threshold <= 1.0:
            raise ValueError("similarity_threshold must be in (0, 1]")

        self.similarity_threshold = similarity_threshold
        self.embed_fn = embed_fn
        self._history: List[Dict] = []

    def check(
        self,
        intent_text: str,
        query_text: str,
        message_idx: int,
    ) -> Dict:
        """Compare intent against generated query for divergence.

        Args:
            intent_text: The user's natural language request
            query_text: The agent's generated query/action
            message_idx: Sequential message index

        Returns:
            Dict with detection results:
                detected (bool): Whether divergence was found
                entity_mismatches (List[str]): Entities in intent but not query
                aggregation_mismatch (Optional[str]): Aggregation divergence
                missing_filters (List[str]): Filters in intent but not query
                scope_mismatch (Optional[str]): Scope divergence
                sort_mismatch (Optional[str]): Sort divergence
                similarity_score (Optional[float]): Embedding similarity
                flags (List[str]): All detected divergence issues
                description (str): Human-readable summary
        """
        flags: List[str] = []

        # 1. Entity mismatch
        intent_entities = self._extract_entities(intent_text)
        query_entities = self._extract_entities(query_text)
        missing_entities = intent_entities - query_entities
        entity_mismatches = sorted(missing_entities)

        if missing_entities:
            present = sorted(query_entities - intent_entities)[:3]
            missing_str = ", ".join(sorted(missing_entities)[:5])
            if present:
                flags.append(
                    f"Entity mismatch: intent mentions [{missing_str}] "
                    f"but query uses [{', '.join(present)}] instead"
                )
            else:
                flags.append(
                    f"Entity missing: intent mentions [{missing_str}] "
                    f"but query does not reference them"
                )

        # 2. Aggregation mismatch
        aggregation_mismatch = self._check_aggregation(
            intent_text, query_text
        )
        if aggregation_mismatch:
            flags.append(aggregation_mismatch)

        # 3. Missing filters
        missing_filters = self._check_filters(intent_text, query_text)
        for mf in missing_filters:
            flags.append(mf)

        # 4. Scope mismatch
        scope_mismatch = self._check_scope(intent_text, query_text)
        if scope_mismatch:
            flags.append(scope_mismatch)

        # 5. Sort mismatch
        sort_mismatch = self._check_sort(intent_text, query_text)
        if sort_mismatch:
            flags.append(sort_mismatch)

        # 6. Embedding similarity (optional)
        similarity_score = None
        if self.embed_fn is not None:
            similarity_score = self._compute_similarity(
                intent_text, query_text
            )
            if similarity_score is not None and similarity_score < self.similarity_threshold:
                flags.append(
                    f"Low semantic similarity: {similarity_score:.3f} "
                    f"(threshold: {self.similarity_threshold:.3f})"
                )

        # Record
        self._history.append({
            "idx": message_idx,
            "intent_entities": intent_entities,
            "query_entities": query_entities,
            "flags": flags,
        })

        detected = len(flags) > 0
        if detected:
            description = (
                f"{len(flags)} query-intent divergence(s) detected. "
                f"The generated query may not match the user's request."
            )
        else:
            description = "No query-intent divergence detected."

        return {
            "detected": detected,
            "entity_mismatches": entity_mismatches,
            "aggregation_mismatch": aggregation_mismatch,
            "missing_filters": [f for f in missing_filters],
            "scope_mismatch": scope_mismatch,
            "sort_mismatch": sort_mismatch,
            "similarity_score": similarity_score,
            "flags": flags,
            "description": description,
        }

    def reset(self) -> None:
        """Reset detector state."""
        self._history.clear()

    def _extract_entities(self, text: str) -> Set[str]:
        """Extract meaningful entity words from text."""
        # Remove common SQL syntax noise
        cleaned = re.sub(r'[\'\"`,;()\[\]{}=<>!+\-*/]', ' ', text)
        words = re.findall(r'\b\w+\b', cleaned.lower())
        entities = set()
        for w in words:
            if (
                len(w) >= MIN_ENTITY_LENGTH
                and w not in _STOPWORDS
                and w not in AGGREGATION_WORDS
                and not w.isdigit()
            ):
                entities.add(w)
        return entities

    def _check_aggregation(
        self, intent: str, query: str
    ) -> Optional[str]:
        """Check for aggregation mismatches between intent and query."""
        intent_lower = intent.lower()
        query_lower = query.lower()

        intent_aggs = {
            w for w in AGGREGATION_WORDS if w in intent_lower
        }
        query_aggs = set()
        for w in AGGREGATION_WORDS:
            if w in query_lower:
                query_aggs.add(w)
        # Also check SQL functions
        sql_matches = SQL_AGGREGATION_PATTERN.findall(query)
        for m in sql_matches:
            query_aggs.add(m.lower().strip())

        if not intent_aggs:
            return None

        # Normalize: avg/average/mean are equivalent
        avg_group = {"avg", "average", "mean"}
        count_group = {"count", "number"}
        total_group = {"sum", "total"}

        def normalize(aggs: Set[str]) -> Set[str]:
            result = set()
            for a in aggs:
                if a in avg_group:
                    result.add("average")
                elif a in count_group:
                    result.add("count")
                elif a in total_group:
                    result.add("sum")
                else:
                    result.add(a)
            return result

        norm_intent = normalize(intent_aggs)
        norm_query = normalize(query_aggs)

        if norm_intent and not norm_query:
            return (
                f"Aggregation missing: intent requests "
                f"[{', '.join(sorted(intent_aggs))}] but query has no aggregation"
            )

        mismatched = norm_intent - norm_query
        if mismatched and norm_query:
            return (
                f"Aggregation mismatch: intent requests "
                f"[{', '.join(sorted(intent_aggs))}] but query uses "
                f"[{', '.join(sorted(query_aggs))}]"
            )

        return None

    def _check_filters(
        self, intent: str, query: str
    ) -> List[str]:
        """Check for date/time and exclusion filters in intent missing from query."""
        issues: List[str] = []

        # Date filters
        intent_has_date = any(p.search(intent) for p in DATE_FILTER_PATTERNS)
        query_has_date = any(p.search(query) for p in DATE_FILTER_PATTERNS)
        # Also check for SQL date patterns
        sql_date = bool(re.search(
            r'\b(DATE|DATETIME|TIMESTAMP|INTERVAL|NOW\(\)|CURRENT_DATE|'
            r'DATE_SUB|DATE_ADD|DATEADD|DATEDIFF)\b',
            query,
            re.IGNORECASE,
        ))

        if intent_has_date and not query_has_date and not sql_date:
            date_match = None
            for p in DATE_FILTER_PATTERNS:
                m = p.search(intent)
                if m:
                    date_match = m.group(0)
                    break
            issues.append(
                f"Date filter omitted: intent specifies "
                f"'{date_match or 'time constraint'}' "
                f"but query has no date/time filter"
            )

        # Exclusion filters
        intent_has_exclusion = any(
            p.search(intent) for p in EXCLUSION_PATTERNS[:2]
        )
        query_has_filter = any(
            p.search(query) for p in EXCLUSION_PATTERNS
        )
        if intent_has_exclusion and not query_has_filter:
            exc_match = None
            for p in EXCLUSION_PATTERNS[:2]:
                m = p.search(intent)
                if m:
                    exc_match = m.group(0)
                    break
            issues.append(
                f"Filter omitted: intent specifies "
                f"'{exc_match or 'filter condition'}' "
                f"but query has no corresponding filter"
            )

        return issues

    def _check_scope(
        self, intent: str, query: str
    ) -> Optional[str]:
        """Check for scope/limit divergence."""
        intent_scope = None
        for p in SCOPE_PATTERNS:
            m = p.search(intent)
            if m:
                intent_scope = m.group(0)
                break

        if intent_scope is None:
            return None

        query_has_scope = any(p.search(query) for p in SCOPE_PATTERNS)
        if not query_has_scope:
            return (
                f"Scope omitted: intent specifies '{intent_scope}' "
                f"but query has no limit/scope constraint"
            )
        return None

    def _check_sort(
        self, intent: str, query: str
    ) -> Optional[str]:
        """Check for sort/order divergence."""
        intent_has_sort = any(p.search(intent) for p in SORT_PATTERNS)
        if not intent_has_sort:
            return None

        query_has_sort = any(p.search(query) for p in SORT_PATTERNS)
        if not query_has_sort:
            sort_match = None
            for p in SORT_PATTERNS:
                m = p.search(intent)
                if m:
                    sort_match = m.group(0)
                    break
            return (
                f"Sort omitted: intent specifies "
                f"'{sort_match or 'ordering'}' "
                f"but query has no ORDER BY or sort"
            )
        return None

    def _compute_similarity(
        self, text_a: str, text_b: str
    ) -> Optional[float]:
        """Compute cosine similarity between two texts using embed_fn."""
        if self.embed_fn is None:
            return None

        try:
            import numpy as np
            emb_a = self.embed_fn(text_a)
            emb_b = self.embed_fn(text_b)

            if emb_a is None or emb_b is None:
                return None

            emb_a = np.asarray(emb_a, dtype=float)
            emb_b = np.asarray(emb_b, dtype=float)

            norm_a = np.linalg.norm(emb_a)
            norm_b = np.linalg.norm(emb_b)
            if norm_a == 0 or norm_b == 0:
                return 0.0

            return float(np.dot(emb_a, emb_b) / (norm_a * norm_b))
        except Exception:
            return None
