"""Conversational intent + history-aware question resolution for SaaS chat.

Distinguishes new document questions from follow-ups about prior answers
and from verify / re-check challenges. Keeps a short in-process memory so
follow-ups work even when audit persistence is off.
"""

from __future__ import annotations

import re
from collections import defaultdict, deque
from dataclasses import dataclass
from typing import Literal

ConvIntent = Literal[
    "new_question",
    "follow_up",
    "verify",
    "explain_calc",
]

# Longer than classic RAG memory — follow-ups often arrive several turns later.
_SESSION_TURNS: dict[str, deque[tuple[str, str]]] = defaultdict(lambda: deque(maxlen=8))
_SESSION_PROFILE: dict[str, dict[str, str]] = {}


def set_session_display_name(session_id: str | None, name: str) -> None:
    if not session_id or not name:
        return
    _SESSION_PROFILE.setdefault(session_id, {})["display_name"] = name.strip()


def get_session_display_name(session_id: str | None) -> str | None:
    if not session_id:
        return None
    name = (_SESSION_PROFILE.get(session_id) or {}).get("display_name")
    return name.strip() if name else None


_VERIFY_RE = re.compile(
    r"(?i)\b("
    r"are\s+you\s+sure|is\s+that\s+(?:right|correct|accurate)|"
    r"that\s+(?:can'?t|cannot|doesn'?t|isn'?t)\s+(?:be|look|seem|right|correct)|"
    r"(?:you(?:'re| are)\s+)?wrong|incorrect|mistake|error|"
    r"double[\s-]?check|re[\s-]?check|recheck|verify|recalculate|recompute|"
    r"check\s+again|look\s+again|please\s+confirm|doesn'?t\s+match|"
    r"not\s+what\s+i\s+(?:see|have)|contradict"
    r")\b"
)

_EXPLAIN_CALC_RE = re.compile(
    r"(?i)\b("
    r"how\s+did\s+you\s+(?:get|calculate|compute|count|arrive|derive)|"
    r"how\s+(?:is|was)\s+that\s+(?:calculated|computed|counted)|"
    r"where\s+does\s+(?:that|this|the)\s+(?:number|total|figure|amount)\s+come\s+from|"
    r"show\s+(?:your|the)\s+(?:work|math|calculation)|"
    r"explain\s+(?:your|the)\s+(?:calculation|math|total|figure)"
    r")\b"
)

_FOLLOW_UP_RE = re.compile(
    r"(?i)\b("
    r"\bit\b|\bthat\b|\bthis\b|\bthose\b|\bthese\b|\bthem\b|"
    r"previous\s+answer|your\s+answer|you\s+said|you\s+mentioned|"
    r"as\s+you\s+(?:said|mentioned)|the\s+(?:above|earlier|last)\b|"
    r"why\s+(?:does|did|is|are|only|not)|how\s+come|"
    r"what\s+about|and\s+(?:august|july|june|may|april|march|"
    r"february|january|september|october|november|december)\b|"
    r"only\s+(?:contain|contains|include|includes|cover|covers|has|have|\d+)|"
    r"not\s+(?:august|july|june|may|april|march|february|january|"
    r"september|october|november|december)|"
    r"in\s+a\s+paragraph|as\s+prose|in\s+words|paragraph\s+form|"
    r"where\s+(?:are|is)\s+the\s+rest|missing|incomplete|"
    r"why\s+(?:only|just)\s+\d+|rest\s+of\s+(?:the\s+)?(?:payments?|transactions?|list)|"
    r"same\s+(?:as|for)|compared\s+to|vs\.?|versus"
    r")\b"
)

_COVERAGE_CHALLENGE_RE = re.compile(
    r"(?i)\b("
    r"why\s+(?:only|just)\s+\d+|"
    r"where\s+(?:are|is)\s+the\s+rest|"
    r"(?:list|extraction|index(?:ing)?)\s+is\s+incomplete|"
    r"incomplete\s+(?:list|extraction|index)|"
    r"missing\s+(?:payments?|transactions?|lines?)|"
    r"not\s+(?:showing|listing)\s+(?:all|every)|"
    r"only\s+\d+\s+(?:of\s+\d+\s+)?(?:payments?|transactions?)|"
    r"should\s+be\s+\d+|"
    r"header\s+(?:says|shows)\s+\d+"
    r")\b"
)

_SHORT_FOLLOW_RE = re.compile(
    r"(?i)^(why|how|what\s+about|and|also|ok\s+but|but)\b"
)

# Pure capability / smalltalk — never treat as finance follow-ups
_HELP_OR_SMALLTALK_RE = re.compile(
    r"(?i)^("
    r"hi|hii|hello|hey|yo|sup|wass?u+p+|whass?u+p+|"
    r"(?:what'?s|whats)\s+(?:up|good)|"
    r"thanks?|thank\s+you|bye|goodbye|"
    r"how\s+are\s+(?:you|u)(?:\s+doing)?|"
    r"how(?:'s|\s+is)\s+it\s+going|"
    r"how\s+have\s+you\s+been|"
    r"how\s+you\s+doing|"
    r"help(?:\s+me)?|"
    r"how\s+can\s+you\s+help(?:\s+me)?|"
    r"what\s+can\s+you\s+(?:do|help(?:\s+me)?(?:\s+with)?)|"
    r"who\s+are\s+you|"
    r"what\s+are\s+you|"
    r"tell\s+me\s+about\s+(?:yourself|you|the\s+bot|this\s+bot)|"
    r"my\s+name\s+is\b.|"
    r"i\s*(?:'m|am)\s+[A-Za-z].|"
    r"what(?:'s|\s+is)\s+my\s+name|"
    r"what\s+was\s+my\s+first\s+question|"
    r"complete|done|ok|okay"
    r")[?.!\s]*$"
)

_ANALYTICAL_HINT_RE = re.compile(
    r"(?i)\b("
    r"spent|spend|total|payment|paid|received|income|merchant|categor|"
    r"grocer|july|august|date|month|period|transaction|outflow|inflow"
    r")\b"
)


@dataclass
class ResolvedTurn:
    original: str
    rewritten: str
    conv_intent: ConvIntent
    prior_question: str | None = None
    prior_answer: str | None = None
    history_block: str = ""


def remember_turn(session_id: str | None, question: str, answer: str) -> None:
    if not session_id or not question:
        return
    _SESSION_TURNS[session_id].append((question.strip(), (answer or "").strip()))


def clear_turns(session_id: str | None) -> None:
    if session_id:
        _SESSION_TURNS.pop(session_id, None)
        _SESSION_PROFILE.pop(session_id, None)


def get_turns(session_id: str | None) -> list[tuple[str, str]]:
    if not session_id:
        return []
    return list(_SESSION_TURNS.get(session_id) or [])


def get_first_user_question(session_id: str | None) -> str | None:
    turns = get_turns(session_id)
    if not turns:
        return None
    q = (turns[0][0] or "").strip()
    return q or None


def remember_analysis_context(session_id: str | None, evidence_json: str) -> None:
    """Store last financial-analysis evidence for short follow-ups (reduce it / that merchant)."""
    if not session_id or not evidence_json:
        return
    _SESSION_PROFILE.setdefault(session_id, {})["last_analysis"] = evidence_json[:8000]
    _SESSION_PROFILE.setdefault(session_id, {})["prior_was_analysis"] = "1"


def get_analysis_context(session_id: str | None) -> str | None:
    if not session_id:
        return None
    return (_SESSION_PROFILE.get(session_id) or {}).get("last_analysis")


def prior_was_analysis(session_id: str | None) -> bool:
    if not session_id:
        return False
    return (_SESSION_PROFILE.get(session_id) or {}).get("prior_was_analysis") == "1"


def clear_analysis_flag(session_id: str | None) -> None:
    if not session_id:
        return
    prof = _SESSION_PROFILE.get(session_id)
    if prof:
        prof.pop("prior_was_analysis", None)


def mark_session_introduced(session_id: str | None) -> None:
    """Remember that Alex already introduced themselves in this session."""
    if not session_id:
        return
    _SESSION_PROFILE.setdefault(session_id, {})["introduced"] = "1"


def session_was_introduced(session_id: str | None) -> bool:
    if not session_id:
        return False
    return (_SESSION_PROFILE.get(session_id) or {}).get("introduced") == "1"


def remember_shown_metrics(session_id: str | None, metrics: list[tuple[str, str]]) -> None:
    """Track metric cards already shown so later turns can avoid repeating them."""
    if not session_id or not metrics:
        return
    key = "|".join(f"{a}={b}" for a, b in metrics[:12])
    prev = (_SESSION_PROFILE.get(session_id) or {}).get("shown_metrics", "")
    merged = (prev + "||" + key) if prev else key
    parts = merged.split("||")[-3:]
    _SESSION_PROFILE.setdefault(session_id, {})["shown_metrics"] = "||".join(parts)


def recently_shown_metric(session_id: str | None, label: str, value: str) -> bool:
    if not session_id:
        return False
    blob = (_SESSION_PROFILE.get(session_id) or {}).get("shown_metrics") or ""
    return f"{label}={value}" in blob


def is_coverage_challenge(question: str) -> bool:
    return bool(_COVERAGE_CHALLENGE_RE.search(question or ""))


def classify_conversational_intent(
    question: str,
    *,
    has_history: bool,
) -> ConvIntent:
    q = (question or "").strip()
    if not q:
        return "new_question"

    # Capability / greeting questions are never finance follow-ups.
    if _HELP_OR_SMALLTALK_RE.match(q):
        return "new_question"

    if _VERIFY_RE.search(q):
        return "verify"
    if _EXPLAIN_CALC_RE.search(q):
        return "explain_calc"
    if is_coverage_challenge(q):
        return "explain_calc" if has_history else "follow_up"

    if not has_history:
        return "new_question"

    # Self-contained analytical asks are new questions — do not glue prior date/merchant context.
    # e.g. after "payments on 1 Aug", "How much did I spend?" must not become a date follow-up.
    from app.saas.finance_intent import is_analytical_self_contained

    if is_analytical_self_contained(q):
        return "new_question"

    words = q.split()
    if _FOLLOW_UP_RE.search(q):
        # Pronoun / why-not / previous-answer style
        if len(words) <= 24 or _SHORT_FOLLOW_RE.search(q):
            return "follow_up"
        if re.search(r"(?i)\b(it|that|this|those|these|them)\b", q):
            return "follow_up"
        if re.search(r"(?i)\bwhy\b", q) and _ANALYTICAL_HINT_RE.search(q):
            return "follow_up"

    if len(words) <= 8 and _SHORT_FOLLOW_RE.search(q) and _ANALYTICAL_HINT_RE.search(q):
        return "follow_up"

    return "new_question"


def _history_block(turns: list[tuple[str, str]], *, limit: int = 6) -> str:
    if not turns:
        return ""
    lines = ["Recent conversation (use this to answer follow-ups coherently):"]
    for q, a in turns[-limit:]:
        lines.append(f"User: {q}")
        snippet = re.sub(r"\s+", " ", a).strip()
        if len(snippet) > 520:
            snippet = snippet[:517].rstrip() + "…"
        lines.append(f"Assistant: {snippet}")
    return "\n".join(lines)


def _rewrite_follow_up(
    question: str,
    prior_q: str | None,
    prior_a: str | None,
    intent: ConvIntent,
) -> str:
    q = question.strip()
    prior_q = (prior_q or "").strip()
    prior_a = (prior_a or "").strip()

    if intent == "verify":
        focus = prior_q or "the previous figures"
        return (
            f"Re-check the underlying statement data for: {focus}. "
            f"The user challenged the previous answer (“{q}”). "
            "Recompute from the full document; do not defend the prior reply."
        )

    if intent == "explain_calc" or is_coverage_challenge(q):
        focus = prior_q or "the previous total"
        return (
            f"Explain how the figures for “{focus}” were derived from the full statement "
            f"(header totals vs indexed transactions). Address incomplete extraction if "
            f"indexed lines are fewer than the header count. User asked: {q}"
        )

    # follow_up — ground pronouns / why-questions in prior turn
    parts = [q]
    if prior_q:
        parts.append(f"(Referring to the previous question: {prior_q})")
    if prior_a:
        # Keep a short anchor so period / month challenges stay specific
        anchor = re.sub(r"\s+", " ", prior_a).strip()
        if len(anchor) > 360:
            anchor = anchor[:357].rstrip() + "…"
        parts.append(f"(Previous answer said: {anchor})")
    return " ".join(parts)


def resolve_turn(session_id: str | None, question: str) -> ResolvedTurn:
    """Classify conversational intent and rewrite the question using history."""
    original = (question or "").strip()
    turns = get_turns(session_id)
    prior_q = prior_a = None
    if turns:
        prior_q, prior_a = turns[-1]

    conv = classify_conversational_intent(original, has_history=bool(turns))
    if conv == "new_question":
        rewritten = original
    else:
        rewritten = _rewrite_follow_up(original, prior_q, prior_a, conv)

    return ResolvedTurn(
        original=original,
        rewritten=rewritten,
        conv_intent=conv,
        prior_question=prior_q,
        prior_answer=prior_a,
        history_block=_history_block(turns),
    )


def is_analytical_finance_intent(finance_intent: str) -> bool:
    return finance_intent in {
        "statement_summary",
        "spending_summary",
        "merchant_analysis",
        "category_spending",
        "date_lookup",
        "income",
        "transaction_search",
        "comparison",
        "insights",
        "financial_advice",
        "period_coverage",
        "payment_count",
    }
