#!/usr/bin/env python3
"""Backfill Jasmin document details into existing opportunities.

Use when an opportunity was created from a Jasmin reconciliation item before
v4926.6, so it still has no commercial_documents/opportunity_items/value.

Examples:
    PYTHONPATH=. python scripts/backfill_jasmin_opportunity_details.py \
      --opportunity-id a4e210f5-b870-48b1-882d-b55c71fbfd38

    PYTHONPATH=. python scripts/backfill_jasmin_opportunity_details.py \
      --document-number ORC.ORC2026.156
"""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
from decimal import Decimal, InvalidOperation
from typing import Any, Dict, Iterable, List, Optional

from sqlalchemy import text

from app.db import engine
from app.reconciliation_service import (
    _apply_jasmin_documents_to_opportunity,  # noqa: PLC2701 - deliberate operator backfill script
    _jasmin_document_lines_from_item,  # noqa: PLC2701
    _jasmin_document_totals,  # noqa: PLC2701
    _payload_record,  # noqa: PLC2701
)


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


def _money_value(value: Any) -> Any:
    if isinstance(value, dict):
        for key in ("amount", "baseAmount", "reportingAmount", "value"):
            if value.get(key) not in (None, ""):
                return value.get(key)
        return None
    return value


def _decimal_or_none(value: Any) -> Optional[str]:
    value = _money_value(value)
    if value in (None, ""):
        return None
    try:
        return str(Decimal(str(value).replace(",", ".")).quantize(Decimal("0.01")))
    except (InvalidOperation, ValueError):
        return None


def _json(value: Any) -> str:
    return json.dumps(value, ensure_ascii=False, default=str)


def _ids_from_metadata(metadata: Any) -> List[str]:
    if not isinstance(metadata, dict):
        return []
    ids: List[str] = []
    for key in ("created_from_reconciliation_item_id", "reconciliation_item_id"):
        value = metadata.get(key)
        if value:
            ids.append(str(value))
    for key in ("item_ids", "reconciliation_item_ids"):
        value = metadata.get(key)
        if isinstance(value, list):
            ids.extend(str(v) for v in value if v)
    return list(dict.fromkeys(ids))


def _load_opportunity(opportunity_id: Optional[str], document_number: Optional[str]) -> Optional[Dict[str, Any]]:
    with engine.begin() as conn:
        if opportunity_id:
            row = conn.execute(text("""
                SELECT id::text, title, value_amount, product_interest, local_customer_id::text,
                       customer_name, customer_email, metadata
                FROM opportunities
                WHERE id = CAST(:id AS UUID)
                LIMIT 1
            """), {"id": opportunity_id}).mappings().first()
            return dict(row) if row else None
        if document_number:
            row = conn.execute(text("""
                SELECT id::text, title, value_amount, product_interest, local_customer_id::text,
                       customer_name, customer_email, metadata
                FROM opportunities
                WHERE metadata->>'document_number' = :document_number
                   OR title ILIKE '%' || :document_number || '%'
                ORDER BY updated_at DESC
                LIMIT 1
            """), {"document_number": document_number}).mappings().first()
            return dict(row) if row else None
    return None


def _load_candidate_items(opportunity: Dict[str, Any]) -> List[Dict[str, Any]]:
    metadata = opportunity.get("metadata") if isinstance(opportunity.get("metadata"), dict) else {}
    ids = _ids_from_metadata(metadata)
    external_id = _as_text(metadata.get("external_id"))
    document_number = _as_text(metadata.get("document_number"))
    opportunity_id = _as_text(opportunity.get("id"))

    with engine.begin() as conn:
        rows = conn.execute(text("""
            SELECT id::text, source_system, external_type, external_id, title, description,
                   status, priority, suggested_action, confidence, opportunity_id::text,
                   customer_id::text, customer_name, customer_email, customer_tax_id,
                   document_number, document_date, amount, currency, payload,
                   resolution_note, created_at, updated_at, resolved_at
            FROM reconciliation_items
            WHERE source_system = 'jasmin'
              AND (
                    opportunity_id = CAST(:opportunity_id AS UUID)
                 OR (CAST(:ids AS TEXT[]) IS NOT NULL AND id::text = ANY(CAST(:ids AS TEXT[])))
                 OR (CAST(:external_id AS TEXT) <> '' AND external_id = CAST(:external_id AS TEXT))
                 OR (CAST(:document_number AS TEXT) <> '' AND document_number = CAST(:document_number AS TEXT))
                 OR (CAST(:document_number AS TEXT) <> '' AND payload::text ILIKE '%' || CAST(:document_number AS TEXT) || '%')
              )
            ORDER BY updated_at DESC, created_at DESC
        """), {
            "opportunity_id": opportunity_id,
            "ids": ids or [],
            "external_id": external_id,
            "document_number": document_number,
        }).mappings().all()

    # De-duplicate while keeping recency order.
    seen = set()
    result = []
    for row in rows:
        item = dict(row)
        item_id = item.get("id")
        if item_id in seen:
            continue
        seen.add(item_id)
        result.append(item)
    return result


async def _fetch_jasmin_detail_async(item: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    external_type = _as_text(item.get("external_type"))
    external_id = _as_text(item.get("external_id"))
    if not external_id:
        return None
    from app.jasmin_client import JasminClient

    client = JasminClient()
    if external_type == "jasmin_quotation":
        return await client.get_quotation(external_id)
    if external_type == "jasmin_invoice":
        return await client.get_invoice(external_id)
    # Some tenants represent pro-forma as a quotation. Try quotation detail as a
    # conservative fallback when the external id is present.
    if external_type == "jasmin_proforma":
        try:
            return await client.get_quotation(external_id)
        except Exception:
            return None
    return None


def _with_jasmin_detail(item: Dict[str, Any], *, fetch_detail: bool) -> Dict[str, Any]:
    if not fetch_detail:
        return item
    existing_lines = _jasmin_document_lines_from_item(item)
    if existing_lines:
        return item
    try:
        detail = asyncio.run(_fetch_jasmin_detail_async(item))
    except Exception as exc:
        item = dict(item)
        payload = item.get("payload") if isinstance(item.get("payload"), dict) else {}
        item["payload"] = {
            **payload,
            "detail_fetch_error": f"{type(exc).__name__}: {exc}",
        }
        return item
    if not isinstance(detail, dict):
        return item

    payload = item.get("payload") if isinstance(item.get("payload"), dict) else {}
    enriched = dict(item)
    enriched["payload"] = {
        **payload,
        "record": detail,
        "detail_source": "jasmin_api",
        "previous_record": payload.get("record"),
    }

    # Fill top-level fields if the detailed document exposes them only there.
    record_number = detail.get("documentNumber") or detail.get("naturalKey") or detail.get("number")
    if record_number and not enriched.get("document_number"):
        enriched["document_number"] = record_number
    total = (
        detail.get("payableAmount")
        or detail.get("totalAmount")
        or detail.get("grossAmount")
        or detail.get("amount")
    )
    if total and not enriched.get("amount"):
        enriched["amount"] = _decimal_or_none(total) or total
    return enriched


def _summary_for_items(items: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
    result = []
    for item in items:
        totals = _jasmin_document_totals(item)
        lines = _jasmin_document_lines_from_item(item)
        result.append({
            "id": item.get("id"),
            "external_type": item.get("external_type"),
            "external_id": item.get("external_id"),
            "document_number": item.get("document_number"),
            "amount": item.get("amount"),
            "totals": totals,
            "lines": len(lines),
            "payload_keys": list((item.get("payload") or {}).keys()) if isinstance(item.get("payload"), dict) else [],
        })
    return result


def _post_import_summary(opportunity_id: str) -> Dict[str, Any]:
    with engine.begin() as conn:
        opportunity = conn.execute(text("""
            SELECT id::text, title, value_amount, product_interest, metadata
            FROM opportunities
            WHERE id = CAST(:id AS UUID)
        """), {"id": opportunity_id}).mappings().first()
        docs = conn.execute(text("""
            SELECT id::text, document_kind, document_number, amount, total_amount, currency, document_date
            FROM commercial_documents
            WHERE opportunity_id = CAST(:id AS UUID)
            ORDER BY created_at DESC
        """), {"id": opportunity_id}).mappings().all()
        items = conn.execute(text("""
            SELECT product_name, quantity, unit_price, total_price, jasmin_sales_item
            FROM opportunity_items
            WHERE opportunity_id = CAST(:id AS UUID)
            ORDER BY created_at
        """), {"id": opportunity_id}).mappings().all()
        lines = conn.execute(text("""
            SELECT cdl.description, cdl.quantity, cdl.unit_price, cdl.total_amount, cdl.jasmin_sales_item
            FROM commercial_document_lines cdl
            JOIN commercial_documents cd ON cd.id = cdl.document_id
            WHERE cd.opportunity_id = CAST(:id AS UUID)
            ORDER BY cdl.line_index
        """), {"id": opportunity_id}).mappings().all()
    return {
        "opportunity": dict(opportunity or {}),
        "documents": [dict(r) for r in docs],
        "opportunity_items": [dict(r) for r in items],
        "document_lines": [dict(r) for r in lines],
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--opportunity-id")
    parser.add_argument("--document-number")
    parser.add_argument("--no-fetch-jasmin-detail", action="store_true")
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--actor", default="operator_backfill")
    args = parser.parse_args()

    if not args.opportunity_id and not args.document_number:
        parser.error("usa --opportunity-id ou --document-number")

    opportunity = _load_opportunity(args.opportunity_id, args.document_number)
    if not opportunity:
        print(json.dumps({"ok": False, "error": "opportunity_not_found"}, ensure_ascii=False, indent=2))
        return 2

    items = _load_candidate_items(opportunity)
    enriched_items = [
        _with_jasmin_detail(item, fetch_detail=not args.no_fetch_jasmin_detail)
        for item in items
    ]
    print(json.dumps({
        "opportunity_id": opportunity.get("id"),
        "title": opportunity.get("title"),
        "candidate_items": _summary_for_items(enriched_items),
        "dry_run": args.dry_run,
    }, ensure_ascii=False, indent=2, default=str))

    if not enriched_items:
        print(json.dumps({"ok": False, "error": "no_jasmin_reconciliation_items_found"}, ensure_ascii=False, indent=2))
        return 3

    if args.dry_run:
        return 0

    with engine.begin() as conn:
        result = _apply_jasmin_documents_to_opportunity(
            conn,
            enriched_items,
            str(opportunity["id"]),
            actor=args.actor,
        )
    print(json.dumps({"ok": True, "import_result": result}, ensure_ascii=False, indent=2, default=str))
    print(json.dumps(_post_import_summary(str(opportunity["id"])), ensure_ascii=False, indent=2, default=str))
    return 0


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