"""In-memory PDF calculation jobs (confirm-before-calc).

By default user PDFs and extracted content are kept in process memory only
and are never written to disk (see Settings.persist_user_data).
"""

from __future__ import annotations

import json
import threading
from pathlib import Path
from typing import Any

from app.config import BASE_DIR, get_settings
from app.pdf.calc import calculate, format_job_context, validate_rows
from app.pdf.extract import ExtractionResult, extract_pdf

_LOCK = threading.Lock()
_JOBS: dict[str, dict[str, Any]] = {}
_SESSION_JOBS: dict[str, str] = {}  # session_id -> job_id
_UPLOAD_DIR = BASE_DIR / "data" / "pdf_uploads"


def _persist_enabled() -> bool:
    return bool(get_settings().persist_user_data)


def _ensure_upload_dir() -> Path:
    _UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
    return _UPLOAD_DIR


def _serialize_rows(rows: list[Any]) -> list[dict[str, Any]]:
    out = []
    for r in rows:
        if hasattr(r, "__dict__") and not isinstance(r, dict):
            out.append(
                {
                    "description": r.description,
                    "debit": r.debit,
                    "credit": r.credit,
                    "amount": r.amount,
                    "date": r.date,
                    "account": r.account,
                    "raw": r.raw,
                }
            )
        else:
            out.append(dict(r))
    return out


def _save_job_disk(job: dict[str, Any]) -> None:
    """Optional durable copy — off by default for privacy."""
    if not _persist_enabled():
        return
    _ensure_upload_dir()
    meta_path = _UPLOAD_DIR / f"{job['job_id']}.json"
    meta_path.write_text(
        json.dumps({k: v for k, v in job.items() if k != "file_path"}, indent=2)
    )


def create_job_from_upload(saved_path: Path, filename: str) -> dict[str, Any]:
    result: ExtractionResult = extract_pdf(saved_path, filename=filename)
    kind = getattr(result, "document_kind", None) or "unknown"
    validation = validate_rows(_serialize_rows(result.rows), document_kind=kind)
    result.validation = validation

    # Digital + validation OK → trusted for calc; otherwise confirm required
    if result.trusted and validation.get("ok"):
        needs_confirm = False
        trusted = True
        status = "ready"
    else:
        needs_confirm = True
        trusted = False
        status = "needs_confirm"
        result.trusted = False
        result.needs_confirm = True

    job = {
        "job_id": result.job_id,
        "filename": result.filename,
        "source_type": result.source_type,
        "document_kind": kind,
        "trusted": trusted,
        "needs_confirm": needs_confirm,
        "confirmed": False,
        "status": status,
        "rows": _serialize_rows(result.rows),
        "text_preview": result.text_preview,
        "document_text": result.document_text or result.text_preview,
        "warnings": result.warnings,
        "validation": validation,
        "ocr_used": result.ocr_used,
        "file_path": str(saved_path) if _persist_enabled() else None,
        "calculation": None,
        "statement_totals": getattr(result, "statement_totals", None) or {},
    }

    with _LOCK:
        _JOBS[result.job_id] = job

    _save_job_disk(job)
    return job


def get_job(job_id: str) -> dict[str, Any] | None:
    with _LOCK:
        job = _JOBS.get(job_id)
    if job:
        return job
    if not _persist_enabled():
        return None
    meta_path = _UPLOAD_DIR / f"{job_id}.json"
    if meta_path.exists():
        data = json.loads(meta_path.read_text())
        with _LOCK:
            _JOBS[job_id] = data
        return data
    return None


def confirm_job(job_id: str, rows: list[dict[str, Any]]) -> dict[str, Any]:
    job = get_job(job_id)
    if not job:
        raise KeyError("Job not found")

    cleaned = []
    for r in rows:
        cleaned.append(
            {
                "description": str(r.get("description") or "").strip(),
                "debit": r.get("debit"),
                "credit": r.get("credit"),
                "amount": r.get("amount"),
                "date": r.get("date"),
                "account": r.get("account"),
                "raw": r.get("raw") or "",
            }
        )

    validation = validate_rows(cleaned, document_kind=job.get("document_kind") or "unknown")
    job["rows"] = cleaned
    job["validation"] = validation
    job["confirmed"] = True
    job["needs_confirm"] = False
    job["trusted"] = bool(validation.get("ok"))
    job["status"] = "confirmed" if validation.get("ok") else "invalid"
    job["warnings"] = list(job.get("warnings") or [])
    if not validation.get("ok"):
        job["warnings"] = job["warnings"] + validation.get("issues", [])

    with _LOCK:
        _JOBS[job_id] = job
    _save_job_disk(job)
    return job


def attach_job_to_session(session_id: str, job_id: str) -> None:
    if not session_id:
        return
    with _LOCK:
        # Drop previous PDF binding for this session (memory only)
        prev = _SESSION_JOBS.get(session_id)
        _SESSION_JOBS[session_id] = job_id
        if prev and prev != job_id:
            _JOBS.pop(prev, None)


def get_session_job(session_id: str | None) -> dict[str, Any] | None:
    if not session_id:
        return None
    with _LOCK:
        job_id = _SESSION_JOBS.get(session_id)
    if not job_id:
        return None
    return get_job(job_id)


def clear_session_job(session_id: str | None) -> None:
    if not session_id:
        return
    with _LOCK:
        job_id = _SESSION_JOBS.pop(session_id, None)
        if job_id:
            _JOBS.pop(job_id, None)
    # Drop short chat memory for this session too
    try:
        from app.rag.chain import clear_session_history

        clear_session_history(session_id)
    except Exception:
        pass


def ingest_pdf_for_chat(saved_path: Path, filename: str, session_id: str | None) -> dict[str, Any]:
    """Extract → auto-confirm → calculate → bind to chat session (memory only)."""
    job = create_job_from_upload(saved_path, filename)
    rows = job.get("rows") or []
    if not rows and not (job.get("document_text") or job.get("text_preview")):
        job["error"] = f"Could not read usable content from “{filename}”."
        if session_id:
            attach_job_to_session(session_id, job["job_id"])
        return job

    # Prefer labeled header totals when present (any wallet/bank statement layout)
    header = job.get("statement_totals") or {}
    if header.get("money_out") is not None or header.get("money_in") is not None:
        kind = job.get("document_kind") or "bank_statement"
        if kind == "unknown":
            kind = "bank_statement"
            job["document_kind"] = kind
        try:
            if rows:
                job = confirm_job(job["job_id"], rows)
                job = calculate_job(job["job_id"])
            else:
                job["confirmed"] = True
                job["needs_confirm"] = False
                job["trusted"] = True
                from app.pdf.calc import calculate_from_statement_totals

                job["calculation"] = calculate_from_statement_totals(header, rows=[])
                job["status"] = "calculated"
                with _LOCK:
                    _JOBS[job["job_id"]] = job
                _save_job_disk(job)
        except Exception as exc:
            job["warnings"] = list(dict.fromkeys(list(job.get("warnings") or []) + [str(exc)]))
        if session_id:
            attach_job_to_session(session_id, job["job_id"])
        job["context_block"] = format_job_context(job)
        return job

    if rows:
        job = confirm_job(job["job_id"], rows)
        try:
            job = calculate_job(job["job_id"])
        except Exception as exc:
            job["warnings"] = list(dict.fromkeys(list(job.get("warnings") or []) + [str(exc)]))
            if session_id:
                attach_job_to_session(session_id, job["job_id"])
            job["context_block"] = format_job_context(job)
            return job

    if session_id:
        attach_job_to_session(session_id, job["job_id"])

    job["context_block"] = format_job_context(job)
    return job


def calculate_job(job_id: str, *, allow_unbalanced: bool = True) -> dict[str, Any]:
    job = get_job(job_id)
    if not job:
        raise KeyError("Job not found")

    if job.get("ocr_used") or job.get("needs_confirm") or not job.get("trusted"):
        if not job.get("confirmed"):
            raise PermissionError(
                "Confirm extracted amounts before calculating. "
                "Click “Confirm amounts”, then Calculate."
            )

    rows = job.get("rows") or []
    kind = job.get("document_kind") or "unknown"
    header = job.get("statement_totals") or {}
    if not rows and not (
        header.get("money_out") is not None or header.get("money_in") is not None
    ):
        raise ValueError("No line items to calculate. Add or confirm rows first.")

    validation = validate_rows(rows, document_kind=kind) if rows else {
        "ok": True,
        "issues": [],
        "row_count": 0,
        "debit_total": 0,
        "credit_total": 0,
        "balanced": None,
        "document_kind": kind,
    }
    job["validation"] = validation

    hard_issues = [i for i in (validation.get("issues") or []) if "no numeric amount" in i.lower()]
    if hard_issues:
        raise ValueError("Validation failed: " + "; ".join(hard_issues))

    # Refresh labeled totals from document text when missing
    if not (job.get("statement_totals") or {}).get("money_out"):
        from app.pdf.extract import parse_statement_header_totals

        parsed = parse_statement_header_totals(
            job.get("document_text") or job.get("text_preview") or ""
        )
        if parsed:
            job["statement_totals"] = parsed
            header = parsed
            if (job.get("document_kind") or "unknown") == "unknown":
                job["document_kind"] = "bank_statement"
                kind = "bank_statement"

    result = calculate(rows, document_kind=kind, statement_totals=header or None)
    if kind != "bank_statement" and validation.get("balanced") is False:
        result["ok"] = True
        result["imbalanced"] = True
        issues = validation.get("issues") or []
        result["summary"] = (
            result.get("summary", "")
            + " | WARNING: debits and credits do not balance ("
            + "; ".join(issues)
            + ")"
        )
        job["warnings"] = list(dict.fromkeys(list(job.get("warnings") or []) + issues))
    elif kind != "bank_statement" and not validation.get("ok") and not allow_unbalanced:
        raise ValueError("Validation failed: " + "; ".join(validation.get("issues") or []))

    job["calculation"] = result
    job["document_kind"] = kind
    job["status"] = "calculated"
    with _LOCK:
        _JOBS[job_id] = job
    _save_job_disk(job)
    return job
