#!/usr/bin/env python3
"""Audit Jasmin invoices created by converting quotations.

This script is diagnostic only. It does not modify ClientFlow or Jasmin data.
It compares local commercial_documents, remote Jasmin document details, and the
PDF returned by the Jasmin print endpoint for converted invoices.

Main goal: detect why invoices converted from quotations may have unexpected
PDF format/size/layout.
"""
from __future__ import annotations

import argparse
import asyncio
import csv
import hashlib
import json
import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable

from sqlalchemy import text

from app.db import engine
from app.jasmin_client import JasminClient
from app.commercial_service import update_commercial_document_details

A4_PORTRAIT_PT = (595.0, 842.0)
A4_LANDSCAPE_PT = (842.0, 595.0)
POINT_TO_MM = 25.4 / 72.0


def _s(value: Any) -> str:
    return str(value or "").strip()


def _json_default(value: Any) -> str:
    try:
        return value.isoformat()  # type: ignore[attr-defined]
    except Exception:
        return str(value)


def _payload(value: Any) -> dict[str, Any]:
    if isinstance(value, dict):
        return value
    if not value:
        return {}
    try:
        return json.loads(value)
    except Exception:
        return {}


def _dig(data: Any, *path: str) -> Any:
    cur = data
    for key in path:
        if isinstance(cur, dict):
            cur = cur.get(key)
        else:
            return None
    return cur


def _first(data: dict[str, Any], keys: Iterable[str]) -> Any:
    for key in keys:
        if data.get(key) not in (None, ""):
            return data.get(key)
    return None


def _money(value: Any) -> str:
    if isinstance(value, dict):
        for k in ("amount", "value", "baseAmount", "reportingAmount"):
            if value.get(k) is not None:
                return _s(value.get(k))
    return _s(value)


def _selected_jasmin_fields(raw: Any) -> dict[str, Any]:
    if not isinstance(raw, dict):
        return {"raw_type": type(raw).__name__, "raw_preview": _s(raw)[:300]}
    return {
        "id": _first(raw, ["id", "key"]),
        "naturalKey": raw.get("naturalKey"),
        "documentNumber": raw.get("documentNumber"),
        "documentType": _first(raw, ["documentType", "documentTypeKey"]),
        "serie": _first(raw, ["serie", "serieKey"]),
        "seriesNumber": _first(raw, ["seriesNumber", "number"]),
        "company": _first(raw, ["company", "companyKey"]),
        "documentDate": raw.get("documentDate"),
        "postingDate": raw.get("postingDate"),
        "currency": _first(raw, ["currency", "currencyKey"]),
        "totalAmount": _money(raw.get("totalAmount")),
        "payableAmount": _money(raw.get("payableAmount")),
        "taxExclusiveAmount": _money(raw.get("taxExclusiveAmount")),
        "buyerCustomerParty": _first(raw, ["buyerCustomerParty", "buyerCustomerPartyKey"]),
        "buyerCustomerPartyName": raw.get("buyerCustomerPartyName"),
        "paymentTerm": _first(raw, ["paymentTerm", "paymentTermKey"]),
        "paymentMethod": _first(raw, ["paymentMethod", "paymentMethodKey"]),
        "printLayout": _first(raw, ["printLayout", "report", "reportName", "reportKey", "documentReport", "printTemplate"]),
        "schema_hint_keys": sorted([k for k in raw.keys() if re.search(r"print|report|layout|template|format|paper|serie|type", k, re.I)])[:50],
        "line_count": len(raw.get("documentLines") or raw.get("lines") or []) if isinstance(raw.get("documentLines") or raw.get("lines") or [], list) else None,
    }


def _extract_pdf_boxes(data: bytes) -> list[dict[str, Any]]:
    # Works for the normal uncompressed page dictionaries returned by Jasmin.
    # It is intentionally dependency-free for production servers.
    latin = data.decode("latin-1", errors="ignore")
    boxes: list[dict[str, Any]] = []
    pattern = re.compile(
        r"/(MediaBox|CropBox)\s*\[\s*([-+0-9.]+)\s+([-+0-9.]+)\s+([-+0-9.]+)\s+([-+0-9.]+)\s*\]",
        re.I,
    )
    seen: set[tuple[str, float, float, float, float]] = set()
    for m in pattern.finditer(latin):
        kind = m.group(1)
        x0, y0, x1, y1 = (float(m.group(i)) for i in range(2, 6))
        key = (kind.lower(), x0, y0, x1, y1)
        if key in seen:
            continue
        seen.add(key)
        w = abs(x1 - x0)
        h = abs(y1 - y0)
        boxes.append({
            "kind": kind,
            "width_pt": round(w, 2),
            "height_pt": round(h, 2),
            "width_mm": round(w * POINT_TO_MM, 1),
            "height_mm": round(h * POINT_TO_MM, 1),
            "orientation": "landscape" if w > h else "portrait",
            "looks_a4": _looks_a4(w, h),
        })
    return boxes


def _looks_a4(w: float, h: float, tolerance_pt: float = 20.0) -> bool:
    pairs = [(w, h), (h, w)]
    return any(abs(a - A4_PORTRAIT_PT[0]) <= tolerance_pt and abs(b - A4_PORTRAIT_PT[1]) <= tolerance_pt for a, b in pairs)


def _pdf_info(data: bytes, content_type: str) -> dict[str, Any]:
    info = {
        "content_type": content_type,
        "bytes": len(data),
        "sha256_12": hashlib.sha256(data).hexdigest()[:12] if data else "",
        "is_pdf_header": data[:5] == b"%PDF-",
        "header_preview": data[:80].decode("latin-1", errors="replace"),
        "page_count_hint": None,
        "boxes": [],
        "format_warning": "",
    }
    if not data:
        info["format_warning"] = "empty_response"
        return info
    if not info["is_pdf_header"]:
        info["format_warning"] = "not_pdf_header"
        return info
    latin = data.decode("latin-1", errors="ignore")
    # Count '/Type /Page' but not '/Type /Pages'.
    page_count = len(re.findall(r"/Type\s*/Page(?!s)\b", latin))
    info["page_count_hint"] = page_count or None
    boxes = _extract_pdf_boxes(data)
    info["boxes"] = boxes
    if boxes and not any(bool(b.get("looks_a4")) for b in boxes if b.get("kind", "").lower() == "mediabox"):
        info["format_warning"] = "non_a4_or_unexpected_mediabox"
    elif len(data) < 15_000:
        info["format_warning"] = "very_small_pdf"
    elif len(data) > 2_500_000:
        info["format_warning"] = "very_large_pdf"
    return info


def _query_candidates(args: argparse.Namespace) -> list[dict[str, Any]]:
    where = ["inv.document_kind = 'invoice'", "inv.system = 'jasmin'"]
    params: dict[str, Any] = {"limit": args.limit}
    if args.opportunity_id:
        where.append("inv.opportunity_id = CAST(:opportunity_id AS UUID)")
        params["opportunity_id"] = args.opportunity_id
    if args.document:
        where.append("(inv.document_number = :document OR inv.external_id = :document)")
        params["document"] = args.document
    if args.days and not args.document and not args.opportunity_id:
        where.append("inv.created_at >= now() - (:days * INTERVAL '1 day')")
        params["days"] = int(args.days)
    if not args.include_all_invoices:
        where.append("inv.parent_document_id IS NOT NULL")
    where_sql = " AND ".join(where)
    with engine.begin() as conn:
        rows = conn.execute(text(f"""
            SELECT
                inv.id::text AS invoice_doc_id,
                inv.opportunity_id::text AS opportunity_id,
                inv.external_id AS invoice_external_id,
                inv.document_number AS invoice_number,
                inv.document_type AS invoice_type,
                inv.serie AS invoice_serie,
                inv.series_number AS invoice_series_number,
                inv.status AS invoice_status,
                inv.total_amount AS invoice_total,
                inv.currency AS invoice_currency,
                inv.payload AS invoice_payload,
                inv.created_at AS invoice_created_at,
                q.id::text AS quotation_doc_id,
                q.external_id AS quotation_external_id,
                q.document_number AS quotation_number,
                q.document_type AS quotation_type,
                q.serie AS quotation_serie,
                q.total_amount AS quotation_total,
                q.payload AS quotation_payload,
                o.title AS opportunity_title,
                o.customer_name AS opportunity_customer_name,
                c.name AS fiscal_customer_name,
                c.tax_id AS fiscal_customer_tax_id
            FROM commercial_documents inv
            LEFT JOIN commercial_documents q ON q.id = inv.parent_document_id
            LEFT JOIN opportunities o ON o.id = inv.opportunity_id
            LEFT JOIN customers c ON c.id = inv.customer_id
            WHERE {where_sql}
            ORDER BY inv.created_at DESC
            LIMIT :limit
        """), params).mappings().all()
    return [dict(r) for r in rows]


def _mark_local_pdf_format(result: dict[str, Any]) -> None:
    document_id = _s(result.get("invoice_doc_id"))
    if not document_id:
        return
    info = result.get("invoice_pdf") if isinstance(result.get("invoice_pdf"), dict) else {}
    warning = _s((info or {}).get("format_warning"))
    boxes = (info or {}).get("boxes") or []
    first_box = ""
    if boxes:
        b = boxes[0]
        first_box = f"{b.get('kind')} {b.get('width_mm')}x{b.get('height_mm')}mm a4={b.get('looks_a4')}"
    blocked = warning in {"not_pdf_header", "empty_response", "non_a4_or_unexpected_mediabox"}
    update_commercial_document_details(document_id, {"payload": {
        "clientflow_pdf_format": info or {},
        "clientflow_pdf_format_warning": warning,
        "clientflow_pdf_first_box": first_box,
        "clientflow_pdf_send_blocked": blocked,
        "clientflow_pdf_format_checked_by": "audit_jasmin_converted_invoice_format",
    }})


async def _audit_one(row: dict[str, Any], *, client: JasminClient, save_dir: Path | None, compare_quote_pdf: bool) -> dict[str, Any]:
    invoice_external_id = _s(row.get("invoice_external_id"))
    quotation_external_id = _s(row.get("quotation_external_id"))
    result: dict[str, Any] = {
        **{k: _json_default(v) for k, v in row.items() if not k.endswith("_payload")},
        "invoice_local_payload_keys": sorted(_payload(row.get("invoice_payload")).keys()),
        "quotation_local_payload_keys": sorted(_payload(row.get("quotation_payload")).keys()),
        "remote_invoice": {},
        "remote_quotation": {},
        "invoice_pdf": {},
        "quotation_pdf": {},
        "finding": "OK",
        "finding_detail": "",
    }
    try:
        invoice_raw = await client.get_invoice(invoice_external_id)
        result["remote_invoice"] = _selected_jasmin_fields(invoice_raw)
    except Exception as exc:
        result["finding"] = "REMOTE_INVOICE_ERROR"
        result["finding_detail"] = str(exc)
        return result

    if quotation_external_id:
        try:
            quotation_raw = await client.get_quotation(quotation_external_id)
            result["remote_quotation"] = _selected_jasmin_fields(quotation_raw)
        except Exception as exc:
            result["remote_quotation_error"] = str(exc)

    try:
        pdf, content_type = await client.print_invoice_pdf(invoice_external_id)
        result["invoice_pdf"] = _pdf_info(pdf, content_type)
        if save_dir:
            save_dir.mkdir(parents=True, exist_ok=True)
            name = re.sub(r"[^A-Za-z0-9_.-]+", "_", _s(row.get("invoice_number")) or invoice_external_id)
            (save_dir / f"invoice_{name}.pdf").write_bytes(pdf)
    except Exception as exc:
        result["finding"] = "INVOICE_PDF_ERROR"
        result["finding_detail"] = str(exc)
        return result

    if compare_quote_pdf and quotation_external_id:
        try:
            pdf, content_type = await client.print_quotation_pdf(quotation_external_id)
            result["quotation_pdf"] = _pdf_info(pdf, content_type)
            if save_dir:
                save_dir.mkdir(parents=True, exist_ok=True)
                name = re.sub(r"[^A-Za-z0-9_.-]+", "_", _s(row.get("quotation_number")) or quotation_external_id)
                (save_dir / f"quotation_{name}.pdf").write_bytes(pdf)
        except Exception as exc:
            result["quotation_pdf_error"] = str(exc)

    inv_pdf = result.get("invoice_pdf") or {}
    warning = _s(inv_pdf.get("format_warning"))
    inv_remote = result.get("remote_invoice") or {}
    if warning:
        result["finding"] = warning.upper()
        result["finding_detail"] = f"PDF warning: {warning}"
    elif not inv_remote.get("documentType") or not inv_remote.get("serie"):
        result["finding"] = "MISSING_REMOTE_TYPE_OR_SERIE"
        result["finding_detail"] = "GET invoice did not expose documentType/serie; check Jasmin payload."
    else:
        # If the remote response exposes print/report/layout-like keys, surface them.
        hints = inv_remote.get("schema_hint_keys") or []
        if hints:
            result["finding_detail"] = "Remote invoice exposes layout/report-related keys: " + ", ".join(hints[:10])
    return result


def _write_outputs(results: list[dict[str, Any]], prefix: str) -> dict[str, str]:
    json_path = f"{prefix}.json"
    md_path = f"{prefix}.md"
    csv_path = f"{prefix}.csv"
    Path(json_path).write_text(json.dumps(results, ensure_ascii=False, indent=2, default=_json_default), encoding="utf-8")
    flat_rows = []
    for r in results:
        inv_pdf = r.get("invoice_pdf") or {}
        boxes = inv_pdf.get("boxes") or []
        first_box = boxes[0] if boxes else {}
        flat_rows.append({
            "finding": r.get("finding"),
            "finding_detail": r.get("finding_detail"),
            "opportunity_id": r.get("opportunity_id"),
            "opportunity_title": r.get("opportunity_title"),
            "fiscal_customer_name": r.get("fiscal_customer_name"),
            "invoice_number": r.get("invoice_number"),
            "invoice_external_id": r.get("invoice_external_id"),
            "invoice_type": r.get("invoice_type"),
            "invoice_serie": r.get("invoice_serie"),
            "remote_invoice_type": _dig(r, "remote_invoice", "documentType"),
            "remote_invoice_serie": _dig(r, "remote_invoice", "serie"),
            "invoice_pdf_content_type": inv_pdf.get("content_type"),
            "invoice_pdf_bytes": inv_pdf.get("bytes"),
            "invoice_pdf_is_pdf_header": inv_pdf.get("is_pdf_header"),
            "invoice_pdf_page_count_hint": inv_pdf.get("page_count_hint"),
            "first_box_kind": first_box.get("kind"),
            "first_box_width_mm": first_box.get("width_mm"),
            "first_box_height_mm": first_box.get("height_mm"),
            "first_box_looks_a4": first_box.get("looks_a4"),
            "quotation_number": r.get("quotation_number"),
        })
    fieldnames = list(flat_rows[0].keys()) if flat_rows else ["finding"]
    with open(csv_path, "w", encoding="utf-8", newline="") as fh:
        writer = csv.DictWriter(fh, fieldnames=fieldnames)
        writer.writeheader()
        writer.writerows(flat_rows)
    lines = ["# Auditoria formato PDF faturas Jasmin convertidas", ""]
    for r in results:
        inv_pdf = r.get("invoice_pdf") or {}
        boxes = inv_pdf.get("boxes") or []
        box_text = "; ".join(f"{b.get('kind')} {b.get('width_mm')}x{b.get('height_mm')}mm a4={b.get('looks_a4')}" for b in boxes[:3]) or "—"
        lines.extend([
            f"## {r.get('invoice_number') or r.get('invoice_external_id')}",
            f"- Finding: **{r.get('finding')}** {r.get('finding_detail') or ''}",
            f"- Oportunidade: `{r.get('opportunity_id')}` · {r.get('opportunity_title')}",
            f"- Cliente fiscal: {r.get('fiscal_customer_name') or '—'} · NIF {r.get('fiscal_customer_tax_id') or '—'}",
            f"- Invoice local: type={r.get('invoice_type') or '—'} serie={r.get('invoice_serie') or '—'} total={r.get('invoice_total') or '—'}",
            f"- Invoice remoto: type={_dig(r, 'remote_invoice', 'documentType') or '—'} serie={_dig(r, 'remote_invoice', 'serie') or '—'} number={_dig(r, 'remote_invoice', 'documentNumber') or _dig(r, 'remote_invoice', 'naturalKey') or '—'}",
            f"- PDF: content_type={inv_pdf.get('content_type') or '—'} bytes={inv_pdf.get('bytes') or '—'} pages={inv_pdf.get('page_count_hint') or '—'} boxes={box_text}",
            "",
        ])
    Path(md_path).write_text("\n".join(lines), encoding="utf-8")
    return {"json": json_path, "csv": csv_path, "markdown": md_path}


async def _main_async(args: argparse.Namespace) -> int:
    candidates = _query_candidates(args)
    print(f"CONVERTED_INVOICES_TO_SCAN={len(candidates)}")
    client = JasminClient(timeout=float(args.timeout))
    save_dir = Path(args.save_pdf_dir) if args.save_pdf_dir else None
    results: list[dict[str, Any]] = []
    for i, row in enumerate(candidates, start=1):
        print(f"scanning={i}/{len(candidates)} invoice={row.get('invoice_number') or row.get('invoice_external_id')} opp={row.get('opportunity_id')} title={row.get('opportunity_title')}")
        result = await _audit_one(row, client=client, save_dir=save_dir, compare_quote_pdf=not args.no_quote_pdf)
        results.append(result)
        if getattr(args, "mark_local", False):
            try:
                _mark_local_pdf_format(result)
            except Exception as exc:
                print(f"  mark_local_error={exc}")
        inv_pdf = result.get("invoice_pdf") or {}
        boxes = inv_pdf.get("boxes") or []
        box = boxes[0] if boxes else {}
        print(
            "  finding={finding} content_type={ct} bytes={bytes} box={w}x{h}mm a4={a4}".format(
                finding=result.get("finding"),
                ct=inv_pdf.get("content_type"),
                bytes=inv_pdf.get("bytes"),
                w=box.get("width_mm"),
                h=box.get("height_mm"),
                a4=box.get("looks_a4"),
            )
        )
    prefix = args.output_prefix or "/tmp/clientflow_jasmin_converted_invoice_format_audit"
    paths = _write_outputs(results, prefix)
    counts: dict[str, int] = {}
    for r in results:
        counts[_s(r.get("finding") or "UNKNOWN")] = counts.get(_s(r.get("finding") or "UNKNOWN"), 0) + 1
    print("SUMMARY")
    print(f"Findings: {len([r for r in results if r.get('finding') != 'OK'])}")
    for key in sorted(counts):
        print(f"{key}: {counts[key]}")
    print(f"CSV: {paths['csv']}")
    print(f"Markdown: {paths['markdown']}")
    print(f"JSON: {paths['json']}")
    if save_dir:
        print(f"PDF_DIR: {save_dir}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description="Audit format/size of Jasmin PDFs generated after ORC→FA conversion.")
    parser.add_argument("--days", type=int, default=14, help="Recent invoice window when no document/opportunity filter is supplied.")
    parser.add_argument("--limit", type=int, default=30)
    parser.add_argument("--document", help="Invoice document_number or external_id, e.g. FA.FA2026.135")
    parser.add_argument("--opportunity-id", help="Limit to one opportunity UUID")
    parser.add_argument("--include-all-invoices", action="store_true", help="Also scan invoices not linked to a parent quotation.")
    parser.add_argument("--no-quote-pdf", action="store_true", help="Do not fetch parent quotation PDF for comparison.")
    parser.add_argument("--save-pdf-dir", help="Optional directory to save fetched PDFs for manual inspection.")
    parser.add_argument("--mark-local", action="store_true", help="Persist local PDF format flags in commercial_documents.payload; does not modify Jasmin.")
    parser.add_argument("--output-prefix", default="/tmp/clientflow_jasmin_converted_invoice_format_audit")
    parser.add_argument("--timeout", type=float, default=45.0)
    args = parser.parse_args()
    return asyncio.run(_main_async(args))


if __name__ == "__main__":
    raise SystemExit(main())
