"""Materialize central opportunity next actions into operator tasks.

The workflow engine can decide that the next step is a human action even when no
message-triggered task exists.  Operator pages should not leave those actions as
abstract labels only: a pending task must exist so the workbench, related-task
list and task completion flow remain consistent.
"""
from __future__ import annotations

import json
import uuid
from typing import Any, Mapping

from sqlalchemy import text

from app.db import engine
from app.task_service import get_action_config
from app.work_center_action_policy import (
    RECONSTRUCTED_SENSITIVE_ACTIONS,
    canonical_action_code,
    reconstructed_review_required,
)


# v4928.1.5.96 marker: MATERIALIZED_ACTIONS = {"SEND_INVOICE"}
# v4928.1.5.105 marker: MATERIALIZED_ACTIONS = {"SEND_INVOICE", "FOLLOW_UP_PAYMENT"}
MATERIALIZED_ACTIONS = {"SEND_INVOICE", "FOLLOW_UP_PAYMENT", "PREPARE_ORDER"}
# v4928.1.5.129: central workflow emits SHIP_ORDER; operator tasks persist CREATE_SHIPMENT.
MATERIALIZED_ACTIONS.add("CREATE_SHIPMENT")
MATERIALIZED_ACTIONS.add("VALIDATE_PHYSICAL_ORDER")
MATERIALIZED_ACTIONS.add("REVIEW_RECONSTRUCTED_PROCESS")
ACTION_ALIASES = {"SHIP_ORDER": "CREATE_SHIPMENT"}


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


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


def ensure_pending_task_for_next_action(
    opportunity_id: str,
    next_action: Mapping[str, Any] | None,
    *,
    source: str = "opportunity_next_action",
    actor: str = "system",
    reactivate_skipped: bool = False,
) -> dict[str, Any]:
    """Ensure a pending task exists for a materialized next action.

    Returns a small status dictionary. The function is idempotent by both a
    pending-task lookup and a stable idempotency key.
    """

    if not opportunity_id or not next_action:
        return {"created": False, "reason": "missing_input"}

    raw_action_code = canonical_action_code(next_action.get("action_code") or next_action.get("code"))
    document_id = _s(next_action.get("document_id"))
    document_number = _s(next_action.get("document_number"))
    document_key = document_id or document_number or "no_document"

    with engine.begin() as conn:
        opp = conn.execute(text("""
            SELECT
                id::text,
                conversation_id,
                contact_id,
                customer_id,
                local_customer_id::text AS local_customer_id,
                customer_email,
                title,
                COALESCE(metadata, '{}'::jsonb) AS metadata
            FROM opportunities
            WHERE id = CAST(:opportunity_id AS UUID)
        """), {"opportunity_id": opportunity_id}).mappings().first()
        if not opp:
            return {"created": False, "reason": "opportunity_not_found", "action_code": raw_action_code}

        blocked_action_code = ""
        action_code = raw_action_code
        sensitive_codes = {canonical_action_code(code) for code in RECONSTRUCTED_SENSITIVE_ACTIONS}
        if reconstructed_review_required(opp.get("metadata")) and action_code in sensitive_codes:
            blocked_action_code = action_code
            action_code = "REVIEW_RECONSTRUCTED_PROCESS"

        if action_code not in MATERIALIZED_ACTIONS:
            return {"created": False, "reason": "action_not_materialized", "action_code": action_code}

        existing = conn.execute(text("""
            SELECT id::text
            FROM tasks
            WHERE opportunity_id = CAST(:opportunity_id AS UUID)
              AND status = 'pending'
              AND action_code = :action_code
            ORDER BY created_at DESC
            LIMIT 1
        """), {
            "opportunity_id": opportunity_id,
            "action_code": action_code,
        }).scalar()
        if existing:
            return {"created": False, "reason": "pending_exists", "task_id": existing, "action_code": action_code}

        # v4928.1.5.132.2.1: build the canonical task payload before either
        # reactivating a terminal task or inserting a new one.  The v132.2
        # pre-insert branch referenced these values before assignment.
        config = get_action_config(action_code)
        default_routes = {
            "SEND_INVOICE": "financeiro",
            "FOLLOW_UP_PAYMENT": "financeiro",
            "PREPARE_ORDER": "operacoes",
            "VALIDATE_PHYSICAL_ORDER": "logistica",
            "CREATE_SHIPMENT": "logistica",
            "REVIEW_RECONSTRUCTED_PROCESS": "rever",
        }
        route = _s(config.get("route")) or default_routes.get(action_code, "financeiro")
        if action_code == "PREPARE_ORDER" and route in {"", "rever", "vendas"}:
            route = "operacoes"
        if action_code in {"VALIDATE_PHYSICAL_ORDER", "CREATE_SHIPMENT"} and route in {"", "rever", "vendas", "financeiro"}:
            route = "logistica"
        if action_code == "REVIEW_RECONSTRUCTED_PROCESS":
            route = "rever"

        default_labels = {
            "SEND_INVOICE": "Enviar fatura ao cliente",
            "FOLLOW_UP_PAYMENT": "Follow-up pagamento",
            "PREPARE_ORDER": "Preparar encomenda / Odoo",
            "VALIDATE_PHYSICAL_ORDER": "Validar encomenda física",
            "CREATE_SHIPMENT": "Enviar encomenda",
            "REVIEW_RECONSTRUCTED_PROCESS": "Validar processo reconstruído",
        }
        default_descriptions = {
            "SEND_INVOICE": "Fatura criada/associada. Enviar PDF ao cliente e registar evidência.",
            "FOLLOW_UP_PAYMENT": "Encomenda concluída no Odoo/WH-OUT e fatura enviada. Acompanhar pagamento pós-entrega.",
            "PREPARE_ORDER": "Fatura e pagamento confirmados. Criar/validar venda Odoo e preparação da encomenda.",
            "VALIDATE_PHYSICAL_ORDER": "O picking está assigned/reservado no Odoo. Confirmar fisicamente a preparação antes de criar o envio.",
            "CREATE_SHIPMENT": "Encomenda fisicamente validada. Criar envio/tracking ou registar a expedição pelo canal disponível.",
            "REVIEW_RECONSTRUCTED_PROCESS": "Confirmar cliente, documento principal, valor e evidências antes de executar a ação sensível bloqueada.",
        }
        default_label = default_labels.get(action_code, action_code)
        default_description = default_descriptions.get(action_code, "Executar próxima ação operacional.")
        if action_code == "REVIEW_RECONSTRUCTED_PROCESS":
            action_label = _s(config.get("action") or default_label)
            description = default_description
        else:
            action_label = _s(next_action.get("label") or config.get("action") or default_label)
            description = _s(next_action.get("description")) or default_description
        priority = _s(next_action.get("priority")) or "alta"
        if priority not in {"alta", "normal", "baixa"}:
            priority = "alta"

        idempotency_key = f"task:next_action:{opportunity_id}:{action_code}:{document_key}"
        task_id = str(uuid.uuid4())
        metadata = {
            "source": source,
            "actor": actor,
            "materialized_from_next_action": True,
            "document_id": document_id or None,
            "document_number": document_number or None,
            "next_action": dict(next_action),
            "raw_action_code": raw_action_code,
            "normalized_action_code": action_code,
            "blocked_action_code": blocked_action_code or None,
            "review_type": "reconstructed_process" if action_code == "REVIEW_RECONSTRUCTED_PROCESS" else None,
        }

        # v4928.1.5.132.2: when an explicit blocker has just been cleared,
        # reactivate the stable materialized task before attempting an INSERT.
        # This avoids depending on the ON CONFLICT branch and makes the transition
        # observable even when an operator previously skipped the task while it
        # was superseded by a review task.
        allowed_reactivation_sources = {
            "reconstructed_review_completion",
            "physical_validation_completion",
            "manual_v132_2_reactivation_repair",
        }
        if reactivate_skipped and source in allowed_reactivation_sources:
            terminal = conn.execute(text("""
                SELECT id::text, status, action_code, COALESCE(metadata, '{}'::jsonb) AS metadata
                FROM tasks
                WHERE opportunity_id = CAST(:opportunity_id AS UUID)
                  AND action_code = :action_code
                  AND status IN ('skipped', 'ignored')
                  AND COALESCE((metadata->>'materialized_from_next_action')::boolean, FALSE) = TRUE
                ORDER BY created_at DESC
                LIMIT 1
                FOR UPDATE
            """), {
                "opportunity_id": opportunity_id,
                "action_code": action_code,
            }).mappings().first()
            if terminal:
                reactivation_metadata = {
                    **metadata,
                    "reactivated_after_blocker": True,
                    "reactivated_from_status": terminal.get("status"),
                    "reactivated_by": actor,
                    "reactivated_source": source,
                    "reactivated_version": "v4928.1.5.132.2.1",
                }
                reactivated = conn.execute(text("""
                    UPDATE tasks
                    SET status = 'pending',
                        route = :route,
                        action = :action,
                        note = :note,
                        action_required = TRUE,
                        safe_to_post = FALSE,
                        source_system = 'clientflow_next_action',
                        source_event_id = :source_event_id,
                        due_at = now(),
                        done_at = NULL,
                        done_by = NULL,
                        priority = :priority,
                        metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB),
                        updated_at = now()
                    WHERE id = CAST(:task_id AS UUID)
                      AND status IN ('skipped', 'ignored')
                    RETURNING id::text
                """), {
                    "task_id": terminal.get("id"),
                    "route": route,
                    "action": action_label,
                    "note": description,
                    "source_event_id": f"{source}:{opportunity_id}:{action_code}:{document_key}:reactivated",
                    "priority": priority,
                    "metadata": _json(reactivation_metadata),
                }).scalar()
                if reactivated:
                    conn.execute(text("""
                        INSERT INTO task_events (task_id, event_type, payload, created_by)
                        VALUES (CAST(:task_id AS UUID), 'task_reactivated', CAST(:payload AS JSONB), :created_by)
                    """), {
                        "task_id": reactivated,
                        "payload": _json({
                            "action_code": action_code,
                            "reason": "explicit_blocker_cleared_preinsert",
                            "source": source,
                            "previous_status": terminal.get("status"),
                            "version": "v4928.1.5.132.2.1",
                        }),
                        "created_by": actor,
                    })
                    return {
                        "created": False,
                        "reactivated": True,
                        "reason": "reactivated_after_blocker_preinsert",
                        "task_id": reactivated,
                        "action_code": action_code,
                    }



        row = conn.execute(text("""
            INSERT INTO tasks (
                id,
                opportunity_id,
                conversation_id,
                contact_id,
                customer_id,
                action_code,
                route,
                action,
                note,
                action_required,
                safe_to_post,
                status,
                source_system,
                source_event_id,
                idempotency_key,
                due_at,
                metadata,
                priority,
                created_at,
                updated_at
            ) VALUES (
                CAST(:id AS UUID),
                CAST(:opportunity_id AS UUID),
                :conversation_id,
                :contact_id,
                :customer_id,
                :action_code,
                :route,
                :action,
                :note,
                TRUE,
                FALSE,
                'pending',
                'clientflow_next_action',
                :source_event_id,
                :idempotency_key,
                now(),
                CAST(:metadata AS JSONB),
                :priority,
                now(),
                now()
            )
            ON CONFLICT (idempotency_key) DO NOTHING
            RETURNING id::text
        """), {
            "id": task_id,
            "opportunity_id": opportunity_id,
            "conversation_id": opp.get("conversation_id"),
            "contact_id": opp.get("contact_id"),
            "customer_id": opp.get("local_customer_id") or opp.get("customer_id") or opp.get("customer_email"),
            "action_code": action_code,
            "route": route,
            "action": action_label,
            "note": description,
            "source_event_id": f"{source}:{opportunity_id}:{action_code}:{document_key}",
            "idempotency_key": idempotency_key,
            "metadata": _json(metadata),
            "priority": priority,
        }).fetchone()

        created_id = row[0] if row else None
        if not created_id:
            existing_after_conflict = conn.execute(text("""
                SELECT id::text, status, action_code, COALESCE(metadata, '{}'::jsonb) AS metadata
                FROM tasks
                WHERE idempotency_key = :idempotency_key
                LIMIT 1
                FOR UPDATE
            """), {"idempotency_key": idempotency_key}).mappings().first()
            if (
                reactivate_skipped
                and existing_after_conflict
                and _s(existing_after_conflict.get("status")).lower() in {"skipped", "ignored"}
                and canonical_action_code(existing_after_conflict.get("action_code")) == action_code
            ):
                reactivated = conn.execute(text("""
                    UPDATE tasks
                    SET status = 'pending',
                        route = :route,
                        action = :action,
                        note = :note,
                        action_required = TRUE,
                        safe_to_post = FALSE,
                        source_system = 'clientflow_next_action',
                        source_event_id = :source_event_id,
                        due_at = now(),
                        done_at = NULL,
                        done_by = NULL,
                        priority = :priority,
                        metadata = COALESCE(metadata, '{}'::jsonb) || CAST(:metadata AS JSONB),
                        updated_at = now()
                    WHERE id = CAST(:task_id AS UUID)
                      AND status IN ('skipped', 'ignored')
                    RETURNING id::text
                """), {
                    "task_id": existing_after_conflict.get("id"),
                    "route": route,
                    "action": action_label,
                    "note": description,
                    "source_event_id": f"{source}:{opportunity_id}:{action_code}:{document_key}:reactivated",
                    "priority": priority,
                    "metadata": _json({
                        **metadata,
                        "reactivated_after_blocker": True,
                        "reactivated_from_status": existing_after_conflict.get("status"),
                        "reactivated_by": actor,
                        "reactivated_source": source,
                        "reactivated_version": "v4928.1.5.132.2.1",
                    }),
                }).scalar()
                if reactivated:
                    conn.execute(text("""
                        INSERT INTO task_events (task_id, event_type, payload, created_by)
                        VALUES (CAST(:task_id AS UUID), 'task_reactivated', CAST(:payload AS JSONB), :created_by)
                    """), {
                        "task_id": reactivated,
                        "payload": _json({
                            "action_code": action_code,
                            "reason": "explicit_blocker_cleared",
                            "source": source,
                            "previous_status": existing_after_conflict.get("status"),
                        }),
                        "created_by": actor,
                    })
                    return {
                        "created": False,
                        "reactivated": True,
                        "reason": "reactivated_after_blocker",
                        "task_id": reactivated,
                        "action_code": action_code,
                    }
            return {
                "created": False,
                "reason": "idempotency_conflict",
                "task_id": existing_after_conflict.get("id") if existing_after_conflict else None,
                "existing_status": existing_after_conflict.get("status") if existing_after_conflict else None,
                "action_code": action_code,
            }

        conn.execute(text("""
            INSERT INTO task_events (task_id, event_type, payload, created_by)
            VALUES (CAST(:task_id AS UUID), 'task_created', CAST(:payload AS JSONB), :created_by)
        """), {
            "task_id": created_id,
            "payload": _json(metadata),
            "created_by": actor,
        })

        return {"created": True, "task_id": created_id, "action_code": action_code, "document_number": document_number or None}
