"""Query planner — maps intents to SQL / search / graph / multi-doc tools."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any, Literal

from app.fdi.compute import (
    ComputedResult,
    detect_merchant_in_question,
    document_inventory,
    document_profile,
    get_assertion,
    group_by_counterparty,
    lookup_identifier,
    sum_direction,
)
from app.fdi.identifiers import detect_id_query
from app.fdi.multidoc import compare_documents, find_duplicates, reconcile_amounts, timeline_activity
from app.fdi.verify import VerifiedAnswer, render_computed, should_abstain

ToolName = Literal[
    "inventory",
    "profile",
    "lookup_id",
    "assertion",
    "sum_out",
    "sum_in",
    "group_out",
    "compare",
    "reconcile",
    "timeline",
    "duplicates",
    "search_rag",
    "abstain",
    "passthrough",
]


@dataclass
class PlanStep:
    tool: ToolName
    args: dict[str, Any] = field(default_factory=dict)
    reason: str = ""


@dataclass
class FdiPlan:
    intent: str
    confidence: float
    steps: list[PlanStep]
    slots: dict[str, Any] = field(default_factory=dict)


@dataclass
class FdiTurnResult:
    handled: bool
    answer: str | None = None
    confidence: float = 0.0
    intent: str = "unknown"
    plan: FdiPlan | None = None
    verified: VerifiedAnswer | None = None
    prefer_rag: bool = False


def classify_fdi_intent(question: str) -> tuple[str, float, dict[str, Any]]:
    q = (question or "").strip()
    slots: dict[str, Any] = {}

    if re.search(r"(?i)^\s*(hi|hello|hey|thanks|bye)\b", q) and len(q.split()) <= 4:
        return "general_chat", 0.9, slots
    if re.search(
        r"(?i)\b(who\s+are\s+you|are\s+you\s+a\s+(?:boy|girl)|what\s+can\s+you\s+do|how\s+can\s+you\s+help)\b",
        q,
    ):
        return "assistant_meta", 0.9, slots
    if re.search(r"(?i)\b(how\s+to|what\s+is\s+(?:an?\s+)?(?:llm|pdf|api)\b)", q) and not re.search(
        r"(?i)\b(my|statement|invoice|document|uploaded)\b", q
    ):
        if not detect_id_query(q):
            return "general_knowledge", 0.75, slots

    # Multi-doc tools before identifier sniffing (avoids "invoice" false positives).
    if re.search(
        r"(?i)\b(reconcil\w*|match(?:ing)?\s+invoices?|"
        r"invoice.{0,40}bank|bank.{0,40}invoice)\b",
        q,
    ):
        return "reconcile", 0.9, slots
    if re.search(
        r"(?i)\b(compare\b.{0,40}\b(?:documents?|files?|statements?|years?|pdfs?)|"
        r"difference\s+between)\b",
        q,
    ):
        return "compare", 0.88, slots
    if re.search(
        r"(?i)\b(duplicates?|duplicated|double\s+(?:paid|payment|charge)s?)\b",
        q,
    ):
        return "duplicates", 0.88, slots
    if re.search(r"(?i)\b(timeline|chronolog|by\s+date|dated\s+activity|recent\s+payments?)\b", q):
        return "timeline", 0.85, slots

    idq = detect_id_query(q)
    if idq:
        slots["id_type"], slots["id_value"] = idq
        return "lookup_id", 0.92, slots

    if re.search(
        r"(?i)\b(which|what)\s+(?:documents?|files?|pdfs?)\b|"
        r"knowledge\s*base|list\s+(?:my\s+)?(?:documents?|files?)",
        q,
    ) and not re.search(r"(?i)\b(my\s+name|account\s+holder|how\s+much)\b", q):
        return "corpus_inventory", 0.9, slots

    if re.search(
        r"(?i)\b(summar(?:y|ize)|overview|what\s+is\s+(?:this|the)\s+(?:pdf|document|file)\s+about|"
        r"analyse|analyze|tell\s+me\s+about\s+(?:this|the)\s+(?:pdf|document|file|statement))\b",
        q,
    ):
        return "summarize", 0.85, slots

    if re.search(r"(?i)\b(account\s+holder|whose\s+statement|holder\s+name|customer\s+name)\b", q):
        return "lookup_fact", 0.88, slots

    merchant = detect_merchant_in_question(q)
    if merchant:
        slots["merchant"] = merchant

    if re.search(r"(?i)\b(how\s+much\s+(?:did\s+i\s+)?(?:spend|paid|pay)|total\s+spent|total\s+spend)\b", q):
        return "aggregate_spend", 0.9, slots
    if re.search(r"(?i)\b(how\s+much\s+(?:did\s+i\s+)?receiv|total\s+(?:received|income)|money\s+in)\b", q):
        return "aggregate_income", 0.9, slots
    if re.search(r"(?i)\b(biggest|top|largest)\s+(?:expenses?|merchants?|payments?|vendors?)\b", q):
        return "aggregate_group", 0.88, slots
    if re.search(r"(?i)\b(net\s+profit|total\s+assets|portfolio\s+value|gross\s+pay|net\s+pay|emi)\b", q):
        return "metric_lookup", 0.82, slots

    if re.search(r"(?i)^\s*(what|who|where|when|why|how|which|show|list|find)\b", q) or "?" in q:
        return "explain", 0.55, slots
    return "unknown", 0.4, slots


_METRIC_KIND_MAP = [
    (r"(?i)\bnet\s+profit\b", "net_profit"),
    (r"(?i)\btotal\s+assets\b", "total_assets"),
    (r"(?i)\bportfolio\s+value\b", "portfolio_value"),
    (r"(?i)\bgross\s+pay\b", "gross_pay"),
    (r"(?i)\bnet\s+pay\b", "net_pay"),
    (r"(?i)\bemi\b", "emi_amount"),
    (r"(?i)\binvoice\s+total\b", "invoice_total"),
    (r"(?i)\btax(?:able)?\s+(?:value|total)\b", "tax_total"),
]


def build_plan(question: str) -> FdiPlan:
    intent, conf, slots = classify_fdi_intent(question)
    steps: list[PlanStep] = []

    if intent in {"general_chat", "assistant_meta", "general_knowledge"}:
        steps.append(PlanStep("passthrough", reason="non-document intent"))
    elif intent == "corpus_inventory":
        steps.append(PlanStep("inventory", reason="list indexed docs"))
    elif intent == "lookup_id":
        steps.append(
            PlanStep(
                "lookup_id",
                {"id_type": slots.get("id_type"), "value_norm": slots.get("id_value") or ""},
                reason="exact identifier lookup",
            )
        )
    elif intent == "lookup_fact":
        steps.append(PlanStep("profile", reason="holder / doc profile"))
    elif intent == "summarize":
        steps.append(PlanStep("profile", reason="doc profile"))
        steps.append(PlanStep("assertion", {"kind": "total_spend"}, reason="header spend"))
        steps.append(PlanStep("assertion", {"kind": "total_income"}, reason="header income"))
        steps.append(PlanStep("search_rag", reason="narrative enrichment"))
    elif intent == "aggregate_spend":
        if slots.get("merchant"):
            steps.append(PlanStep("sum_out", {"merchant": slots["merchant"]}, reason="merchant spend"))
        else:
            steps.append(PlanStep("assertion", {"kind": "total_spend"}, reason="prefer header total"))
            steps.append(PlanStep("sum_out", reason="fallback sum outflows"))
    elif intent == "aggregate_income":
        steps.append(PlanStep("assertion", {"kind": "total_income"}, reason="prefer header total"))
        steps.append(PlanStep("sum_in", reason="fallback sum inflows"))
    elif intent == "aggregate_group":
        steps.append(PlanStep("group_out", reason="group by counterparty"))
    elif intent == "metric_lookup":
        kind = None
        for pat, k in _METRIC_KIND_MAP:
            if re.search(pat, question or ""):
                kind = k
                break
        if kind:
            steps.append(PlanStep("assertion", {"kind": kind}, reason=f"metric {kind}"))
        steps.append(PlanStep("search_rag", reason="fallback"))
    elif intent == "compare":
        steps.append(PlanStep("compare", reason="multi-doc compare"))
    elif intent == "reconcile":
        steps.append(PlanStep("reconcile", reason="invoice↔bank match"))
    elif intent == "duplicates":
        steps.append(PlanStep("duplicates", reason="duplicate detection"))
    elif intent == "timeline":
        steps.append(PlanStep("timeline", reason="dated activity"))
    elif intent == "explain":
        steps.append(PlanStep("search_rag", reason="open document question"))
    else:
        steps.append(PlanStep("search_rag", reason="fallback retrieval"))

    return FdiPlan(intent=intent, confidence=conf, steps=steps, slots=slots)


def execute_plan(
    plan: FdiPlan,
    *,
    org_id: str,
    agent_id: str,
) -> FdiTurnResult:
    if any(s.tool == "passthrough" for s in plan.steps):
        return FdiTurnResult(False, intent=plan.intent, plan=plan, prefer_rag=False)

    results: list[ComputedResult] = []
    wants_rag = False
    for step in plan.steps:
        if step.tool == "inventory":
            results.append(document_inventory(org_id=org_id, agent_id=agent_id))
        elif step.tool == "profile":
            results.append(document_profile(org_id=org_id, agent_id=agent_id))
        elif step.tool == "lookup_id":
            results.append(
                lookup_identifier(
                    org_id=org_id,
                    agent_id=agent_id,
                    id_type=str(step.args.get("id_type") or "OTHER"),
                    value_norm=str(step.args.get("value_norm") or ""),
                )
            )
        elif step.tool == "assertion":
            results.append(
                get_assertion(org_id=org_id, agent_id=agent_id, kind=str(step.args.get("kind")))
            )
        elif step.tool == "sum_out":
            results.append(
                sum_direction(
                    org_id=org_id,
                    agent_id=agent_id,
                    direction="out",
                    counterparty_like=step.args.get("merchant"),
                )
            )
        elif step.tool == "sum_in":
            results.append(sum_direction(org_id=org_id, agent_id=agent_id, direction="in"))
        elif step.tool == "group_out":
            results.append(group_by_counterparty(org_id=org_id, agent_id=agent_id, direction="out"))
        elif step.tool == "compare":
            results.append(compare_documents(org_id=org_id, agent_id=agent_id))
        elif step.tool == "reconcile":
            results.append(reconcile_amounts(org_id=org_id, agent_id=agent_id))
        elif step.tool == "timeline":
            results.append(timeline_activity(org_id=org_id, agent_id=agent_id))
        elif step.tool == "duplicates":
            results.append(find_duplicates(org_id=org_id, agent_id=agent_id))
        elif step.tool == "search_rag":
            wants_rag = True
        elif step.tool == "abstain":
            msg = str(
                step.args.get("message")
                or "I don't have enough structured evidence to answer that reliably."
            )
            return FdiTurnResult(True, answer=msg, confidence=0.9, intent=plan.intent, plan=plan)

    ok_results = [r for r in results if r.ok]

    if wants_rag and not ok_results:
        return FdiTurnResult(False, intent=plan.intent, plan=plan, prefer_rag=True)

    if not ok_results:
        verified = render_computed(
            results[0] if results else ComputedResult(False, "none", message="No structured result.")
        )
        return FdiTurnResult(
            True,
            answer=verified.text,
            confidence=verified.confidence,
            intent=plan.intent,
            plan=plan,
            verified=verified,
            prefer_rag=wants_rag,
        )

    preferred = ok_results[0]
    for r in ok_results:
        if r.metric in {"total_spend", "total_income"} and r.group_rows:
            if r.group_rows[0].get("source") == "header":
                preferred = r
                break

    if plan.intent == "summarize" or (wants_rag and len(ok_results) > 1):
        parts = [render_computed(r).text for r in ok_results]
        text = "\n\n".join(parts)
        conf = min((r.confidence or 0.7) for r in ok_results)
        return FdiTurnResult(
            True,
            answer=text,
            confidence=conf,
            intent=plan.intent,
            plan=plan,
            prefer_rag=wants_rag,
        )

    verified = render_computed(preferred)
    if verified.abstain or should_abstain(verified.confidence):
        if plan.intent.startswith("aggregate"):
            for r in ok_results[1:]:
                v2 = render_computed(r)
                if not v2.abstain and not should_abstain(v2.confidence):
                    return FdiTurnResult(True, v2.text, v2.confidence, plan.intent, plan, v2)
        return FdiTurnResult(False, intent=plan.intent, plan=plan, prefer_rag=True)

    return FdiTurnResult(
        True,
        answer=verified.text,
        confidence=verified.confidence,
        intent=plan.intent,
        plan=plan,
        verified=verified,
        prefer_rag=wants_rag,
    )


def run_fdi_turn(
    *,
    question: str,
    org_id: str,
    agent_id: str,
) -> FdiTurnResult:
    plan = build_plan(question)
    return execute_plan(plan, org_id=org_id, agent_id=agent_id)
