"""LLM reasoning over retrieved / structured evidence — never return raw chunks."""

from __future__ import annotations

import json
import re
from typing import Any, Callable

from app.config import get_settings
from app.models.schemas import StructuredInsight

SYNTHESIS_PROMPT = """You are {agent_name}, an AI financial analyst.

The user asked a document-related question. Below is structured evidence from their uploaded statement.
Write a concise, natural answer (2–5 short sentences). Lead with the direct answer.
Use only the evidence — never invent merchants, amounts, or dates.
If evidence is thin, say what you can and cannot tell from the statement.
Do not paste raw tables or OCR. Do not mention extraction, chunks, RAG, vectors, or indexing gaps.
Sound like a helpful colleague.

{name_line}
User question: {question}

Evidence (JSON):
{evidence}

Answer:"""

ANALYSIS_PROMPT = """You are {agent_name}, an AI financial analyst helping the user improve their money habits.

Using ONLY the evidence from their uploaded statement, give personalized, actionable advice.
Requirements:
- 3–6 short sentences, conversational tone
- Cite specific merchants or amounts from the evidence as proof
- Mention concentration (where money goes), one clear cut opportunity, and one concrete next step
- Enrich with insights already computed (average, highest spend, frequency, share of total) when present
- If evidence includes focus_merchant, center the advice on that merchant — cite its total, payment count, and share. Never claim that merchant is missing when focus_merchant is present.
- Never invent figures. Never give tax, legal, or investment product advice.
- Never mention extraction, incomplete indexing, chunks, RAG, or internal systems.
- Do not dump a full transaction list.

{name_line}
User question: {question}

Evidence (JSON):
{evidence}

Advice:"""


def _name_line(user_name: str | None) -> str:
    if user_name:
        return f"The user's name is {user_name}. You may use it once naturally."
    return ""


def synthesize_from_evidence(
    *,
    agent_name: str,
    question: str,
    evidence: dict[str, Any],
    invoke_ollama: Callable[..., str],
    user_name: str | None = None,
    analysis: bool = False,
    on_token: Callable[[str], None] | None = None,
) -> str | None:
    template = ANALYSIS_PROMPT if analysis else SYNTHESIS_PROMPT
    prompt = template.format(
        agent_name=agent_name or "Alex",
        name_line=_name_line(user_name),
        question=(question or "").strip(),
        evidence=json.dumps(evidence, ensure_ascii=False, indent=2)[:6000],
    )
    predict = max(
        280 if analysis else 220,
        get_settings().ollama_num_predict_conversational,
    )
    try:
        if on_token:
            parts: list[str] = []
            # Prefer streaming when available on the service wrapper.
            stream = getattr(invoke_ollama, "__self__", None)
            stream_fn = getattr(stream, "stream_ollama", None) if stream else None
            if callable(stream_fn):
                for chunk in stream_fn(prompt, num_predict=predict):
                    parts.append(chunk)
                    on_token(chunk)
                raw = "".join(parts)
            else:
                raw = invoke_ollama(prompt, num_predict=predict)
                if raw and on_token:
                    for piece in re.split(r"(\s+)", raw):
                        if piece:
                            on_token(piece)
        else:
            raw = invoke_ollama(prompt, num_predict=predict)
    except Exception:
        return None
    text = (raw or "").strip()
    return text or None


def insight_evidence_payload(insight: StructuredInsight, *, max_txns: int = 12) -> dict[str, Any]:
    """Compact facts for LLM synthesis — not raw chunk text."""
    payload: dict[str, Any] = {
        "headline": insight.headline,
        "summary": insight.summary,
        "intent": insight.intent,
        "metrics": [
            {"label": m.label, "value": m.value, "hint": m.hint}
            for m in (insight.metrics or [])[:10]
        ],
        "highlights": list(insight.highlights or [])[:8],
    }
    if insight.transactions:
        payload["sample_payments"] = [
            {
                "date": t.date,
                "description": t.description,
                "amount": t.amount,
                "direction": t.direction,
            }
            for t in insight.transactions[:max_txns]
        ]
    if insight.table and insight.table.rows:
        payload["table"] = {
            "headers": insight.table.headers,
            "rows": insight.table.rows[:10],
        }
    return payload


def scrub_internal_user_text(text: str) -> str:
    """Hide backend extraction / indexing language from end users."""
    if not text:
        return text
    patterns = [
        (r"(?i)extraction\s+incomplete[^.]*\.?", ""),
        (r"(?i)incomplete\s+extraction[^.]*\.?", ""),
        (r"(?i)only\s+\d+\s+of\s+\d+\s+payments[^.]*\.?", ""),
        (r"(?i)could\s+only\s+index[^.]*\.?", ""),
        (r"(?i)\d+\s+dated\s+outflows?\s+indexed[^.]*\.?", ""),
        (r"(?i)\bindexed\s+outflows?\b", "payments"),
        (r"(?i)prefer\s+header\s+totals[^.]*\.?", ""),
        (r"(?i)line\s+lists?\s+below\s+are\s+not\s+the\s+full\s+set[^.]*\.?", ""),
        (r"(?i)\b(?:vector|embedding|chunk|OCR|RAG)\b", ""),
        (r"[ \t]{2,}", " "),
        (r"\n{3,}", "\n\n"),
    ]
    out = text
    for pat, repl in patterns:
        out = re.sub(pat, repl, out)
    return out.strip()
