#!/usr/bin/env python3
"""Repair safe findings from audit_operator_page_consistency.py.

This helper is deliberately conservative. By default it only reports what it
would do. Use --apply to write safe repairs.

Safe repairs implemented:
- after_delivery_invoice_task_too_early: ignore premature pending SEND_INVOICE
  tasks while payment_terms=after_delivery and Odoo is still in production.
- obsolete_payment_task_pending: ignore pending payment/follow-up tasks when
  payment is already confirmed.
- future_due_task_already_completed: add explicit metadata/note so the timeline
  says the task was completed early instead of looking like a future action ran.
- invoice_sent_task_done_but_document_status_not_sent: store ClientFlow evidence
  on the commercial document payload when SEND_INVOICE is done.

Critical business exceptions such as delivery done before payment confirmed are
reported but never fixed automatically.
"""
from __future__ import annotations

import argparse
import json
from typing import Any, Dict, List

from sqlalchemy import text

from app.db import engine

PAYMENT_TASK_CODES = {"CONFIRM_PAYMENT", "FOLLOW_UP_PAYMENT", "FOLLOW_UP_PROFORMA", "FOLLOW_UP_QUOTE"}
PENDING_STATUSES = {"pending", "open", "new", "pendente"}
DONE_STATUSES = {"done", "completed", "concluida", "concluída"}


def _rows(sql: str, params: Dict[str, Any] | None = None) -> List[Dict[str, Any]]:
    with engine.begin() as conn:
        return [dict(r) for r in conn.execute(text(sql), params or {}).mappings().all()]


def _execute(sql: str, params: Dict[str, Any]) -> int:
    with engine.begin() as conn:
        result = conn.execute(text(sql), params)
        return int(result.rowcount or 0)


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


def _select_premature_after_delivery_invoice_tasks(limit: int) -> List[Dict[str, Any]]:
    return _rows("""
        WITH odoo AS (
            SELECT
                opportunity_id,
                bool_or(system = 'odoo' AND external_type = 'sale_order') AS has_sale,
                bool_or(system = 'odoo' AND external_type = 'physical_status' AND status IN ('in_production','waiting_stock','order_created','created','confirmed')) AS physical_active,
                bool_or(system = 'odoo' AND external_type = 'production' AND status IN ('in_progress','in_production','confirmed','created')) AS production_active,
                bool_or(system = 'odoo' AND external_type IN ('physical_status','physical_validation') AND status IN ('ready_to_ship','validated','shipped','done','delivered')) AS ready_or_done
            FROM operation_links
            GROUP BY opportunity_id
        )
        SELECT
            t.id::text AS task_id,
            t.action_code,
            t.status AS task_status,
            t.due_at,
            o.id::text AS opportunity_id,
            o.title AS opportunity_title,
            o.customer_name,
            o.metadata
        FROM tasks t
        JOIN opportunities o ON o.id = t.opportunity_id
        LEFT JOIN odoo ON odoo.opportunity_id = o.id
        WHERE upper(COALESCE(t.action_code,'')) = 'SEND_INVOICE'
          AND lower(COALESCE(t.status,'')) IN ('pending','open','new','pendente')
          AND lower(COALESCE(o.metadata->>'payment_terms','')) = 'after_delivery'
          AND COALESCE(odoo.ready_or_done, FALSE) = FALSE
          AND (COALESCE(odoo.production_active, FALSE) = TRUE OR COALESCE(odoo.physical_active, FALSE) = TRUE OR COALESCE(odoo.has_sale, FALSE) = TRUE)
        ORDER BY COALESCE(t.due_at, t.created_at) ASC
        LIMIT :limit
    """, {"limit": limit})


def _select_obsolete_payment_tasks(limit: int) -> List[Dict[str, Any]]:
    return _rows("""
        SELECT
            t.id::text AS task_id,
            t.action_code,
            t.status AS task_status,
            t.due_at,
            o.id::text AS opportunity_id,
            o.title AS opportunity_title,
            o.customer_name
        FROM tasks t
        JOIN opportunities o ON o.id = t.opportunity_id
        WHERE lower(COALESCE(t.status,'')) IN ('pending','open','new','pendente')
          AND upper(COALESCE(t.action_code,'')) IN ('CONFIRM_PAYMENT','FOLLOW_UP_PAYMENT','FOLLOW_UP_PROFORMA','FOLLOW_UP_QUOTE')
          AND (
            o.stage = 'PAYMENT_CONFIRMED'
            OR EXISTS (
                SELECT 1
                FROM operation_links p
                WHERE p.opportunity_id = o.id
                  AND p.system = 'clientflow'
                  AND p.external_type = 'payment'
                  AND p.status = 'confirmed'
            )
          )
        ORDER BY COALESCE(t.due_at, t.created_at) ASC
        LIMIT :limit
    """, {"limit": limit})


def _select_future_due_done_tasks(limit: int) -> List[Dict[str, Any]]:
    return _rows("""
        SELECT
            t.id::text AS task_id,
            t.action_code,
            t.status AS task_status,
            t.due_at,
            o.id::text AS opportunity_id,
            o.title AS opportunity_title,
            o.customer_name
        FROM tasks t
        JOIN opportunities o ON o.id = t.opportunity_id
        WHERE t.due_at IS NOT NULL
          AND t.due_at > now()
          AND lower(COALESCE(t.status,'')) IN ('done','completed','concluida','concluída')
          AND NOT (COALESCE(t.metadata, '{}'::jsonb) ? 'future_done_acknowledged_by')
          AND NOT (COALESCE(t.metadata, '{}'::jsonb) ? 'completed_before_due_acknowledged_at')
        ORDER BY t.due_at ASC
        LIMIT :limit
    """, {"limit": limit})


def _select_invoice_sent_evidence_missing(limit: int) -> List[Dict[str, Any]]:
    return _rows("""
        SELECT DISTINCT ON (d.id)
            d.id::text AS document_id,
            d.document_number,
            d.status AS document_status,
            o.id::text AS opportunity_id,
            o.title AS opportunity_title,
            o.customer_name,
            t.id::text AS task_id,
            t.done_at,
            t.updated_at
        FROM commercial_documents d
        JOIN opportunities o ON o.id = d.opportunity_id
        JOIN tasks t ON t.opportunity_id = o.id
        WHERE lower(COALESCE(d.document_kind,'')) IN ('invoice','fatura','jasmin_invoice','fa','ft')
          AND upper(COALESCE(t.action_code,'')) = 'SEND_INVOICE'
          AND lower(COALESCE(t.status,'')) IN ('done','completed','concluida','concluída')
          AND lower(COALESCE(d.status,'')) NOT IN ('sent','issued_sent')
          AND NOT (COALESCE(d.payload, '{}'::jsonb) ? 'clientflow_invoice_sent_evidence')
          AND NOT (COALESCE(d.payload, '{}'::jsonb) ? 'invoice_sent_at')
        ORDER BY d.id, COALESCE(t.done_at, t.updated_at, t.created_at) DESC
        LIMIT :limit
    """, {"limit": limit})


def _select_delivered_without_payment(limit: int) -> List[Dict[str, Any]]:
    return _rows("""
        WITH odoo_done AS (
            SELECT opportunity_id
            FROM operation_links
            WHERE system = 'odoo'
              AND external_type IN ('physical_status','physical_validation')
              AND status IN ('shipped','done','delivered','validated')
            GROUP BY opportunity_id
        )
        SELECT
            o.id::text AS opportunity_id,
            o.title AS opportunity_title,
            o.customer_name,
            o.metadata
        FROM opportunities o
        JOIN odoo_done d ON d.opportunity_id = o.id
        WHERE COALESCE(lower(o.metadata->>'payment_terms'), 'before_shipping') IN ('before_shipping','','undefined','agreement')
          AND o.stage <> 'PAYMENT_CONFIRMED'
          AND NOT EXISTS (
            SELECT 1 FROM operation_links p
            WHERE p.opportunity_id = o.id
              AND p.system = 'clientflow'
              AND p.external_type = 'payment'
              AND p.status = 'confirmed'
          )
        ORDER BY COALESCE(o.updated_at, o.created_at) DESC
        LIMIT :limit
    """, {"limit": limit})


def _acknowledge_future_done_tasks(task_ids: List[str], *, apply: bool) -> int:
    if not task_ids:
        return 0
    print(f"future_done_tasks_to_acknowledge={len(task_ids)}")
    if not apply:
        return 0
    metadata = _json({
        "future_done_acknowledged_by": "repair_operator_page_consistency_findings",
        "completed_before_due_acknowledged_at": "now()",
        "reason": "task concluída antes da data planeada",
    })
    changed = 0
    for task_id in task_ids:
        changed += _execute("""
            UPDATE tasks
            SET
                note = concat_ws(E'\n', NULLIF(note, ''), CAST(:note AS TEXT)),
                metadata = COALESCE(metadata, '{}'::jsonb) || (CAST(:metadata AS JSONB) - 'completed_before_due_acknowledged_at') || jsonb_build_object('completed_before_due_acknowledged_at', now()),
                updated_at = now()
            WHERE id = CAST(:task_id AS UUID)
        """, {
            "task_id": task_id,
            "note": "Concluída antecipadamente antes da data de follow-up planeada.",
            "metadata": metadata,
        })
    return changed


def _mark_invoice_sent_evidence(rows: List[Dict[str, Any]], *, apply: bool) -> int:
    if not rows:
        return 0
    print(f"invoice_documents_to_mark_sent_evidence={len(rows)}")
    if not apply:
        return 0
    changed = 0
    for row in rows:
        payload = _json({
            "clientflow_invoice_sent_evidence": {
                "source": "task_done",
                "task_id": row.get("task_id"),
                "recorded_by": "repair_operator_page_consistency_findings",
            }
        })
        changed += _execute("""
            UPDATE commercial_documents
            SET
                payload = COALESCE(payload, '{}'::jsonb)
                    || CAST(:payload AS JSONB)
                    || jsonb_build_object('invoice_sent_at', COALESCE(CAST(:sent_at AS TIMESTAMPTZ), now())),
                updated_at = now()
            WHERE id = CAST(:document_id AS UUID)
        """, {
            "document_id": row.get("document_id"),
            "payload": payload,
            "sent_at": row.get("done_at") or row.get("updated_at"),
        })
    return changed


def _ignore_tasks(task_ids: List[str], *, reason: str, apply: bool) -> int:
    if not task_ids:
        return 0
    print(f"tasks_to_ignore={len(task_ids)} reason={reason}")
    if not apply:
        return 0

    # Use one UUID-typed parameter per update instead of passing a Python list
    # into ANY(:task_ids). psycopg3 can fail to infer the type of list/array
    # parameters in SQLAlchemy text() statements on some installations.
    metadata = _json({"auto_ignored_by": "repair_operator_page_consistency_findings", "reason": reason})
    changed = 0
    for task_id in task_ids:
        changed += _execute("""
            UPDATE tasks
            SET
                status = 'ignored',
                note = concat_ws(E'\n', NULLIF(note, ''), CAST(:reason AS TEXT)),
                metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB),
                updated_at = now()
            WHERE id = CAST(:task_id AS UUID)
        """, {
            "task_id": task_id,
            "reason": reason,
            "metadata": metadata,
        })
    return changed


def main() -> int:
    parser = argparse.ArgumentParser(description="Repair safe operator-page consistency findings.")
    parser.add_argument("--limit", type=int, default=200)
    parser.add_argument("--apply", action="store_true", help="Write safe repairs. Without this, dry-run only.")
    parser.add_argument("--fix-premature-after-delivery-invoice", action="store_true", help="Ignore premature pending SEND_INVOICE tasks for payment-after-delivery orders still in production.")
    parser.add_argument("--fix-obsolete-payment-tasks", action="store_true", help="Ignore payment/follow-up tasks after payment is confirmed.")
    parser.add_argument("--fix-future-done-task-notes", action="store_true", help="Add note/metadata to future-due tasks already completed so the timeline is explicit.")
    parser.add_argument("--fix-invoice-sent-evidence", action="store_true", help="Store ClientFlow invoice-sent evidence on documents when SEND_INVOICE task is done.")
    parser.add_argument("--show-critical", action="store_true", help="List delivered-before-payment cases. Never auto-fixes them.")
    args = parser.parse_args()

    if not (
        args.fix_premature_after_delivery_invoice
        or args.fix_obsolete_payment_tasks
        or args.fix_future_done_task_notes
        or args.fix_invoice_sent_evidence
        or args.show_critical
    ):
        args.fix_premature_after_delivery_invoice = True
        args.fix_obsolete_payment_tasks = True
        args.fix_future_done_task_notes = True
        args.fix_invoice_sent_evidence = True
        args.show_critical = True

    print(f"apply={args.apply} limit={args.limit}")
    total_changed = 0

    if args.fix_premature_after_delivery_invoice:
        rows = _select_premature_after_delivery_invoice_tasks(args.limit)
        print(f"\nPREMATURE_AFTER_DELIVERY_SEND_INVOICE={len(rows)}")
        for r in rows:
            print(f"- task={r['task_id']} opp={r['opportunity_id']} title={r.get('opportunity_title')} customer={r.get('customer_name')} due={r.get('due_at')}")
        changed = _ignore_tasks([r["task_id"] for r in rows], reason="obsoleta: pagamento após entrega; aguardar produção/preparação Odoo antes de emitir/enviar fatura", apply=args.apply)
        total_changed += changed
        if args.apply:
            print(f"ignored_premature_after_delivery_invoice_tasks={changed}")

    if args.fix_obsolete_payment_tasks:
        rows = _select_obsolete_payment_tasks(args.limit)
        print(f"\nOBSOLETE_PAYMENT_TASKS={len(rows)}")
        for r in rows:
            print(f"- task={r['task_id']} code={r.get('action_code')} opp={r['opportunity_id']} title={r.get('opportunity_title')} customer={r.get('customer_name')} due={r.get('due_at')}")
        changed = _ignore_tasks([r["task_id"] for r in rows], reason="obsoleta: pagamento já confirmado", apply=args.apply)
        total_changed += changed
        if args.apply:
            print(f"ignored_obsolete_payment_tasks={changed}")

    if args.fix_future_done_task_notes:
        rows = _select_future_due_done_tasks(args.limit)
        print(f"\nFUTURE_DUE_TASK_ALREADY_COMPLETED={len(rows)}")
        for r in rows:
            print(f"- task={r['task_id']} code={r.get('action_code')} opp={r['opportunity_id']} title={r.get('opportunity_title')} customer={r.get('customer_name')} due={r.get('due_at')}")
        changed = _acknowledge_future_done_tasks([r["task_id"] for r in rows], apply=args.apply)
        total_changed += changed
        if args.apply:
            print(f"acknowledged_future_due_done_tasks={changed}")

    if args.fix_invoice_sent_evidence:
        rows = _select_invoice_sent_evidence_missing(args.limit)
        print(f"\nINVOICE_SENT_TASK_DONE_BUT_DOCUMENT_NOT_MARKED={len(rows)}")
        for r in rows:
            print(f"- doc={r.get('document_number')} document_id={r.get('document_id')} task={r.get('task_id')} opp={r.get('opportunity_id')} title={r.get('opportunity_title')} customer={r.get('customer_name')}")
        changed = _mark_invoice_sent_evidence(rows, apply=args.apply)
        total_changed += changed
        if args.apply:
            print(f"invoice_sent_evidence_marked={changed}")

    if args.show_critical:
        rows = _select_delivered_without_payment(args.limit)
        print(f"\nCRITICAL_DELIVERED_WITHOUT_PAYMENT_CONFIRMED={len(rows)}")
        for r in rows:
            print(f"- opp={r['opportunity_id']} title={r.get('opportunity_title')} customer={r.get('customer_name')}")
        if rows:
            print("manual_action=Confirmar se o pagamento existe. Se sim, registar pagamento confirmado na oportunidade; se não, contactar/regularizar antes de fechar.")

    print("\nSUMMARY")
    print(f"changed={total_changed}")
    if not args.apply:
        print("dry_run=True")
    return 0


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