from __future__ import annotations

import sys
from pathlib import Path
from typing import Any, Callable

from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel, Field

PROJECT_ROOT = Path(__file__).resolve().parents[3]
FRAMEWORK_SRC = PROJECT_ROOT / "agent_framework_oci" / "libs" / "agent_framework" / "src"
for entry in (PROJECT_ROOT, FRAMEWORK_SRC):
    if str(entry) not in sys.path:
        sys.path.insert(0, str(entry))
load_dotenv(PROJECT_ROOT / ".env", override=False)

from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
from agent_framework.idempotency import create_idempotency_store
from agent_framework.cache.cache import create_cache
from agent_framework.config.settings import get_settings
from agent_framework.workflows import FileWorkflowRepository, WorkflowRuntime
from app.domain.contas import ContasDomainService
from app.domain.contas.workflow_actions import build_contas_workflow_actions
from app.domain.contas.invoice_resolver import InvoiceResolver
from app.domain.contas.invoice_context import InvoiceContextService
from app.domain.contas.item_matcher import SimilarityItemMatcher
from app.domain.contas.vas_cancellation_message import compose_vas_cancellation_message

app = FastAPI(title="TIM Contas MCP - Framework Native")
service = ContasDomainService()
invoice_resolver = InvoiceResolver(matcher=SimilarityItemMatcher())
settings = get_settings()
_workflow_runtime: WorkflowRuntime | None = None
_invoice_context_service: InvoiceContextService | None = None


def get_invoice_context_service() -> InvoiceContextService:
    global _invoice_context_service
    if _invoice_context_service is None:
        _invoice_context_service = InvoiceContextService(service.client, create_cache(settings))
    return _invoice_context_service


def get_workflow_runtime() -> WorkflowRuntime:
    """Inicializa WorkflowRuntime/checkpointer/idempotência somente no primeiro uso.

    Evita abrir Oracle/Redis durante import, health ou tools/list e garante que
    transações usem o IdempotencyStore selecionado pelo framework, não memória local.
    """
    global _workflow_runtime
    if _workflow_runtime is None:
        idempotency_store = create_idempotency_store(
            settings, namespace="contas", require_durable=False
        )
        _workflow_runtime = WorkflowRuntime(
            FileWorkflowRepository(PROJECT_ROOT / "workflows"),
            actions=build_contas_workflow_actions(
                service, idempotency_store=idempotency_store
            ),
            checkpointer=create_langgraph_checkpointer(settings),
        )
    return _workflow_runtime


class ToolCall(BaseModel):
    tool_name: str
    arguments: dict[str, Any] = Field(default_factory=dict)


TOOLS: dict[str, dict[str, Any]] = {
    "consultar_faturas": {"description": "Consulta faturas do cliente.", "input_schema": {"msisdn": "string"}},
    "invoice_explanation": {"description": "Executa o workflow de explicação de fatura com pause/resume pelo WorkflowRuntime do framework.", "input_schema": {"msisdn": "string"}},
    "consultar_vas": {"description": "Consulta VAS ativos.", "input_schema": {"msisdn": "string"}},
    "consultar_historico_vas": {"description": "Consulta histórico de VAS.", "input_schema": {"msisdn": "string"}},
    "cancelar_vas_avulso": {"description": "Executa workflow de cancelamento VAS após confirmação transacional do framework.", "input_schema": {"msisdn": "string", "subject": "string", "items": "array"}},
    "tratar_vas_estrategico": {"description": "Executa workflow conversacional de VAS estratégico/bundle.", "input_schema": {"msisdn": "string", "subject": "string", "items": "array"}},
    "contestar_cobranca": {"description": "Executa workflow completo de contestação/Conta Certa.", "input_schema": {"msisdn": "string", "subject": "string", "valor": "number"}},
    "pro_rata": {"description": "Executa workflow conversacional de pró-rata.", "input_schema": {"msisdn": "string", "planos": "array", "has_plano_controle": "boolean"}},
    "termino_desconto": {"description": "Formata capability de término de desconto.", "input_schema": {"msisdn": "string", "nome_plano": "string"}},
    "valor_divergente": {"description": "Executa capability de valor divergente.", "input_schema": {"msisdn": "string"}},
    "retomar_workflow": {"description": "Retoma um workflow pausado pelo mesmo execution_id.", "input_schema": {"workflow_name": "string", "execution_id": "string", "resposta_usuario": "string"}},
    "consultar_status_solicitacao": {"description": "Consulta/atualiza status técnico de solicitação/protocolo TIM.", "input_schema": {"msisdn": "string", "protocol": "string"}},
    "enviar_sms": {"description": "Envia SMS por integração TIM.", "input_schema": {"msisdn": "string", "message": "string"}},
    "recuperar_fatura_pdf": {"description": "Recupera SecurePDF usando contrato criptografado TIM.", "input_schema": {"msisdn": "string", "invoice_id": "string", "customer_id": "string"}},
    "finalizar_atendimento": {"description": "Executa workflow de finalização e seus efeitos de domínio.", "input_schema": {"status": "string", "summary": "string", "msisdn": "string"}},
}


def _require(args: dict[str, Any], *names: str) -> None:
    missing = [n for n in names if args.get(n) in (None, "")]
    if missing:
        raise ValueError("Parâmetros obrigatórios ausentes: " + ", ".join(missing))




def _clarification_option(item: Any) -> dict[str, Any]:
    name = str(getattr(item, "canonical_name", "") or "").strip()
    msisdn = str(getattr(item, "msisdn", "") or "").strip()
    charge_date = str(getattr(item, "charge_date", "") or "").strip()
    value = getattr(item, "value", None)
    details = []
    if msisdn:
        details.append(f"linha final {msisdn[-4:]}")
    if charge_date:
        details.append(f"data {charge_date}")
    if value is not None:
        details.append(f"R$ {value}")
    label = name + ((" — " + ", ".join(details)) if details else "")
    return {"label": label, "value": name, "msisdn": msisdn, "charge_date": charge_date}


def _preflight_subject(name: str, args: dict[str, Any]) -> dict[str, Any] | None:
    """Resolve/desambigua o item contra a evidência da fatura antes do workflow.

    A regra é de domínio; a persistência e a retomada da clarificação ficam no
    AgentRuntimeMixin. Quando o cliente já escolheu uma opção, não reabre o gate.
    """
    if name not in {"cancelar_vas_avulso", "tratar_vas_estrategico", "contestar_cobranca"}:
        return None
    if bool(args.get("clarification_resolved")):
        return None
    msisdn = str(args.get("msisdn") or "").strip()
    subject = str(args.get("subject") or "").strip()
    if not msisdn or not subject:
        return None
    try:
        invoice_detail = args.get("billing_analysis")
        if not isinstance(invoice_detail, dict):
            invoice_detail = service.client.billing_analysis(msisdn)
        if not isinstance(invoice_detail, dict):
            return None
        outcome = invoice_resolver.resolve([subject], invoice_detail)
    except Exception:
        # Fail-safe de disponibilidade: a própria operação ainda valida o serviço
        # no backend TIM. O gate de similaridade nunca transforma falha de leitura
        # em autorização para um item diferente.
        return None
    if outcome.ambiguous:
        matches = outcome.ambiguous[0].matches
        options = [_clarification_option(item) for item in matches]
        return {
            "status": "NEEDS_CLARIFICATION",
            "parameter": "subject",
            "question": f"Encontrei mais de uma cobrança parecida com '{subject}'. Qual delas você quis dizer?",
            "options": options,
        }
    if len(outcome.resolved) == 1:
        resolved = outcome.resolved[0]
        args["subject"] = resolved.canonical_name
        if resolved.msisdn:
            args.setdefault("item_msisdn", resolved.msisdn)
        if resolved.charge_date:
            args.setdefault("charge_date", resolved.charge_date)
        if getattr(resolved, "value", None) is not None:
            args.setdefault("resolved_value", str(resolved.value))
        # Paridade do backend original: se o cliente pediu cancelamento avulso,
        # mas a própria fatura classifica o item como estratégico/bundle, não
        # executamos a operação errada. A resolução determinística do domínio
        # redireciona a mesma solicitação para o workflow VAS estratégico.
        if name == "cancelar_vas_avulso" and resolved.tool_category == "vas_estrategico":
            args["_domain_redirect"] = "tratar_vas_estrategico"
            args["type"] = resolved.item_type
            args["items"] = [{
                "type": resolved.item_type,
                "msisdn": resolved.msisdn or msisdn,
                "name": resolved.canonical_name,
            }]
    if outcome.out_of_scope and not outcome.resolved:
        return {
            "status": "OUT_OF_SCOPE",
            "success": False,
            "error": f"O item '{subject}' existe na fatura, mas não pertence a uma categoria tratável por esta operação.",
        }
    return None

async def _enrich_invoice_context(name: str, args: dict[str, Any]) -> None:
    if name not in {"invoice_explanation", "cancelar_vas_avulso", "tratar_vas_estrategico", "contestar_cobranca"}:
        return
    msisdn = str(args.get("msisdn") or "").strip()
    if not msisdn:
        return
    # If all required evidence is already present, preserve it exactly.
    if isinstance(args.get("complete_invoices_payload"), dict) and isinstance(args.get("billing_analysis"), dict):
        return
    session_id = str(args.get("session_id") or args.get("original_session_id") or args.get("conversation_key") or "").strip()
    invoice_id = str(args.get("invoice_id") or args.get("current_invoice_number") or "").strip()
    include_detail = name in {"cancelar_vas_avulso", "contestar_cobranca"} and not isinstance(args.get("invoice_detail"), dict)
    try:
        ctx = await get_invoice_context_service().get(
            session_id=session_id,
            msisdn=msisdn,
            invoice_id=invoice_id,
            use_cache=bool(args.get("use_invoice_context_cache", True)),
            include_detail=include_detail,
            message_id=str(args.get("message_id") or args.get("interaction_key") or ""),
        )
    except Exception:
        return
    values = ctx.as_dict()
    if ctx.business_events:
        args["_prefetch_business_events"] = list(ctx.business_events)
    for key, value in values.items():
        if value not in (None, ""):
            args.setdefault(key, value)
    if ctx.invoice_id:
        args.setdefault("invoice_id", ctx.invoice_id)
        args.setdefault("current_invoice_number", ctx.invoice_id)
    if ctx.customer_id:
        args.setdefault("customer_id", ctx.customer_id)


def _with_prefetch_events(result: Any, args: dict[str, Any]) -> Any:
    events = args.get("_prefetch_business_events") if isinstance(args.get("_prefetch_business_events"), list) else []
    if not events or not isinstance(result, dict):
        return result
    merged = dict(result)
    existing = merged.get("business_events") if isinstance(merged.get("business_events"), list) else []
    merged["business_events"] = [*events, *existing]
    return merged


def _workflow_payload(name: str, args: dict[str, Any]) -> dict[str, Any]:
    payload = dict(args)
    msisdn = str(args.get("msisdn") or "")
    subject = str(args.get("subject") or "")
    if name == "cancelamento_vas_avulso" and not isinstance(payload.get("items"), list):
        payload["items"] = [{"msisdn": msisdn, "name": subject, "type": "avulso"}]
    elif name == "vas_estrategico" and not isinstance(payload.get("items"), list):
        payload["items"] = [{"msisdn": msisdn, "name": subject, "type": str(args.get("type") or "estrategico")}]
    elif name == "contestacao_tool":
        # Paridade do backend original: o workflow usa tipo_atendimento para
        # selecionar branches/contratos de contestação. O wrapper antigo sempre
        # fixava esse valor quando a tool contestar_cobranca era chamada.
        payload.setdefault("tipo_atendimento", "contestacao")
        payload.setdefault("servico", subject)
        payload.setdefault("descricao", args.get("motivo") or "")
        payload.setdefault("items", [{"itemName": subject, "claimedAmount": args.get("valor"), "validatedAmount": args.get("valor")}])
    return payload


def _result_payload(result: Any, *, workflow_name: str) -> dict[str, Any]:
    data = result.model_dump() if hasattr(result, "model_dump") else dict(result)
    trace = data.get("trace") if isinstance(data.get("trace"), list) else []
    last_node = ""
    for row in reversed(trace):
        if isinstance(row, dict) and row.get("node"):
            last_node = str(row.get("node"))
            break
    if not last_node and isinstance(data.get("state"), dict):
        last_node = str(data["state"].get("current_node") or "")
    metadata = {
        "workflow_name": workflow_name,
        "workflow_execution_id": data.get("execution_id"),
        "workflow_status": data.get("status"),
        "workflow_last_node": last_node or None,
        "resume_tool": "retomar_workflow" if data.get("status") == "PAUSED" else None,
    }
    payload = {**data, "metadata": metadata}
    # Compatibilidade funcional do antigo backend, agora derivada apenas do
    # branch determinístico do WorkflowRuntime do framework.
    if data.get("status") == "COMPLETED":
        if workflow_name == "invoice_explanation":
            if last_node == "registrar_protocolo_aceite":
                payload["recomenda_finalizacao"] = True
                payload["status_finalizacao_sugerido"] = "resolvido"
            elif last_node == "finalizar_nao_resolvido":
                payload["recomenda_finalizacao"] = True
                payload["status_finalizacao_sugerido"] = "nao_resolvido"
            elif last_node == "resposta_falha_servico":
                payload["success"] = False
                payload["service_failed"] = True
                payload["auto_finalize_on_failure"] = False
                payload["mensagem"] = "Para continuar com a sua solicitação, aguarde um instante."
        elif workflow_name == "pro_rata" and last_node in {"registrar_aceitou", "registrar_nao_controle"}:
            payload["recomenda_finalizacao"] = True
            payload["status_finalizacao_sugerido"] = "resolvido"
    return payload


async def _run_workflow(workflow_name: str, args: dict[str, Any]) -> dict[str, Any]:
    result = await get_workflow_runtime().arun(
        workflow_name,
        _workflow_payload(workflow_name, args),
        execution_id=args.get("workflow_execution_id"),
    )
    payload = _result_payload(result, workflow_name=workflow_name)
    if workflow_name == "contestacao_tool" and payload.get("status") == "FAILED":
        details = payload.get("error_details") if isinstance(payload.get("error_details"), dict) else {}
        body = details.get("body")
        provider_message = ""
        if isinstance(body, dict):
            provider = body.get("provider") if isinstance(body.get("provider"), dict) else {}
            provider_message = str(provider.get("errorMessage") or body.get("description") or "").strip()
        protocol_node = (payload.get("output") or {}).get("registrar_protocolo") if isinstance(payload.get("output"), dict) else {}
        protocol = str((protocol_node or {}).get("protocolo_id") or (protocol_node or {}).get("protocol_number") or "") if isinstance(protocol_node, dict) else ""
        message = provider_message or str(payload.get("error") or "Falha ao executar contestação")
        payload["success"] = False
        payload["mensagem"] = message
        if provider_message:
            payload["contestation_error_description"] = provider_message
        if protocol:
            payload["contestacao_protocol"] = protocol
        payload["erro_sistemico"] = not bool(provider_message)
        payload["erro"] = "contestacao_nao_realizada" if provider_message else "erro_falha_sistema"
    if workflow_name == "contestacao_tool" and payload.get("status") == "COMPLETED":
        output = payload.get("output") if isinstance(payload.get("output"), dict) else {}
        contest = output.get("abrir_contestacao_cliente") if isinstance(output.get("abrir_contestacao_cliente"), dict) else {}
        if contest and contest.get("success") is False:
            reason = str(contest.get("guardrail_reason") or contest.get("contestation_error_description") or contest.get("error") or "Contestação não executada")
            payload["success"] = False
            payload["error"] = reason
            payload["erro"] = "erro_falha_sistema" if contest.get("blocked") else "contestacao_nao_realizada"
            payload["erro_sistemico"] = bool(contest.get("blocked"))
            payload["mensagem"] = reason
            if contest.get("contestation_error_description"):
                payload["contestation_error_description"] = contest.get("contestation_error_description")
    return _with_prefetch_events(payload, args)




def _workflow_result_dict(result: Any) -> dict[str, Any]:
    return result.model_dump() if hasattr(result, "model_dump") else dict(result)


def _node_output(result: Any, node_id: str) -> dict[str, Any]:
    data = _workflow_result_dict(result)
    output = data.get("output") if isinstance(data.get("output"), dict) else {}
    node = output.get(node_id)
    return dict(node) if isinstance(node, dict) else {}


def _first_invoice_context(invoices: Any) -> tuple[dict[str, Any], dict[str, Any]]:
    if not isinstance(invoices, dict):
        return {}, {}
    billing = invoices.get("billingProfile") or invoices.get("billing_profile") or {}
    customer = billing.get("customer") if isinstance(billing, dict) and isinstance(billing.get("customer"), dict) else {}
    items = invoices.get("paymentItems") or invoices.get("payment_items") or []
    invoice = next((x for x in items if isinstance(x, dict)), {}) if isinstance(items, list) else {}
    return dict(customer), dict(invoice)


def _value_for_subject(args: dict[str, Any], subject: str) -> Any:
    for item in args.get("items") or []:
        if not isinstance(item, dict):
            continue
        name = str(item.get("name") or item.get("service") or item.get("subject") or item.get("desc") or "").strip()
        if name.casefold() == subject.casefold():
            return item.get("value") if item.get("value") is not None else item.get("valor")
    if str(args.get("subject") or "").strip().casefold() == subject.casefold():
        return args.get("resolved_value") if args.get("resolved_value") is not None else args.get("valor")
    return None




def _invoice_detail_msisdn(invoice_detail: Any, subject: str) -> str:
    """Resolve the billed line for an item from invoice detail.

    The legacy behavior used the invoice evidence to fix LLM/tool arguments that
    accidentally attached a dependent item to the holder line. This remains a
    pure deterministic domain normalization before the workflows are invoked.
    """
    needle = str(subject or "").strip().casefold()
    if not needle or not isinstance(invoice_detail, dict):
        return ""
    for line_key, sections in invoice_detail.items():
        if not isinstance(sections, dict):
            continue
        for rows in sections.values():
            if not isinstance(rows, list):
                continue
            for row in rows:
                if not isinstance(row, dict):
                    continue
                desc = str(row.get("desc") or row.get("name") or row.get("description") or "").strip().casefold()
                if desc == needle:
                    return str(row.get("msisdn") or line_key or "").strip()
    return ""


def _normalize_cancel_items(args: dict[str, Any], holder_msisdn: str) -> list[dict[str, Any]]:
    items = [dict(x) for x in (args.get("items") or []) if isinstance(x, dict)]
    if not items and args.get("subject"):
        items = [{
            "msisdn": args.get("item_msisdn") or holder_msisdn,
            "name": args.get("subject"),
            "value": args.get("resolved_value") if args.get("resolved_value") is not None else args.get("valor"),
        }]
    invoice_detail = args.get("invoice_detail")
    for item in items:
        subject = str(item.get("name") or item.get("service") or item.get("subject") or item.get("desc") or "").strip()
        resolved_line = _invoice_detail_msisdn(invoice_detail, subject)
        if resolved_line:
            item["msisdn"] = resolved_line
        elif not item.get("msisdn"):
            item["msisdn"] = holder_msisdn
    return items

def _digits_only(value: Any) -> str:
    return "".join(ch for ch in str(value or "") if ch.isdigit())


def _money_ptbr(value: Any) -> str:
    text = str(value if value is not None else "0").strip()
    try:
        if "," in text:
            numeric = float(text.replace(".", "").replace(",", "."))
        else:
            numeric = float(text)
    except (TypeError, ValueError):
        return text
    return f"{numeric:.2f}".replace(".", ",")


def _nonzero_money_or(value: Any, fallback: Any) -> Any:
    try:
        text = str(value if value is not None else "").strip()
        if not text:
            return fallback
        numeric = float(text.replace(".", "").replace(",", ".") if "," in text else text)
        return value if abs(numeric) > 1e-9 else fallback
    except (TypeError, ValueError):
        return value if value not in (None, "") else fallback


async def _run_cancelamento_com_contestacao(args: dict[str, Any]) -> dict[str, Any]:
    # The session/customer MSISDN is always the holder for the contestation.
    # Individual cancellation items may belong to dependent lines.
    holder_msisdn = str(args.get("holder_msisdn") or args.get("session_msisdn") or args.get("msisdn") or "")
    normalized_args = dict(args)
    normalized_args["msisdn"] = holder_msisdn
    # Paridade do backend original: CPF é alias aceito para social_sec_no e
    # deve chegar ao workflow sem máscara.
    social_sec_no = _digits_only(args.get("social_sec_no") or args.get("cpf"))
    if social_sec_no:
        normalized_args["social_sec_no"] = social_sec_no
    normalized_args["items"] = _normalize_cancel_items(args, holder_msisdn)
    cancel_result = await get_workflow_runtime().arun(
        "cancelamento_vas_avulso",
        _workflow_payload("cancelamento_vas_avulso", normalized_args),
        execution_id=args.get("workflow_execution_id"),
    )
    cancel_data = _workflow_result_dict(cancel_result)
    if cancel_data.get("status") != "COMPLETED":
        return _result_payload(cancel_result, workflow_name="cancelamento_vas_avulso")

    cancel_node = _node_output(cancel_result, "cancelar_vas_avulso")
    candidate_rows = cancel_node.get("contestation_candidates")
    if not isinstance(candidate_rows, list):
        candidate_rows = cancel_node.get("itens_para_contestacao")
    candidates = [x for x in (candidate_rows or []) if isinstance(x, dict)]
    raw_results = [x for x in (cancel_node.get("results") or []) if isinstance(x, dict)]
    successful = [x for x in raw_results if x.get("success")]
    failed = [x for x in raw_results if not x.get("success")]
    # Compatibilidade do wrapper original: versões/actions podem reportar o
    # resultado agregado em cancelados/nao_cancelados sem repetir `success` em
    # results[]. O wrapper deve preservar esse outcome, não transformá-lo em falha.
    if not successful:
        successful = [
            {**dict(x), "subject": x.get("subject") or x.get("servico") or x.get("name"), "success": True}
            for x in (cancel_node.get("cancelados") or []) if isinstance(x, dict)
        ]
    if not failed:
        failed = [
            {**dict(x), "subject": x.get("subject") or x.get("servico") or x.get("name"), "success": False}
            for x in (cancel_node.get("nao_cancelados") or []) if isinstance(x, dict)
        ]

    if not candidates:
        line_protocols = [
            str(row.get("protocolo_id") or row.get("protocol") or "").strip()
            for row in (cancel_node.get("protocolos_por_linha") or [])
            if isinstance(row, dict) and str(row.get("protocolo_id") or row.get("protocol") or "").strip()
        ]
        message = compose_vas_cancellation_message({
            "success": bool(successful),
            "cancelados": [{"servico": x.get("subject")} for x in successful],
            "nao_encontrados": [{"servico": x.get("subject")} for x in failed if "não encontrado" in str(x.get("error") or "").casefold()],
            "protocols_for_response": line_protocols,
        })
        cancel_data.setdefault("output", {})["resposta_cancelamento"] = {"mensagem": message}
        cancel_data.update({
            "success": bool(successful),
            "mensagem": message,
            "recomenda_finalizacao": bool(successful),
            "status_finalizacao_sugerido": "resolvido" if successful else "nao_resolvido",
            "cancelamento_vas_protocol": line_protocols[0] if line_protocols else None,
            "protocols_for_response": line_protocols,
            "requires_protocol_in_response": bool(line_protocols),
            "auto_finalize_on_failure": False,
        })
        cancel_data["metadata"] = {
            "workflow_name": "cancelamento_vas_avulso",
            "workflow_execution_id": cancel_data.get("execution_id"),
            "workflow_status": cancel_data.get("status"),
            "composite_workflows": ["cancelamento_vas_avulso"],
        }
        return cancel_data

    msisdn = holder_msisdn
    invoices = args.get("complete_invoices_payload")
    if not isinstance(invoices, dict):
        invoices = service.consultar_faturas(msisdn=msisdn)
    customer, invoice = _first_invoice_context(invoices)
    items: list[dict[str, Any]] = []
    total = 0.0
    for candidate in candidates:
        subject = str(candidate.get("subject") or (candidate.get("service") or {}).get("name") or "").strip()
        value = _value_for_subject(normalized_args, subject)
        try:
            numeric = float(str(value).replace(".", "").replace(",", ".")) if isinstance(value, str) and "," in value else float(value or 0)
        except Exception:
            numeric = 0.0
        total += numeric
        money = _money_ptbr(value if value is not None else numeric)
        items.append({
            "itemName": subject,
            "item_name": subject,
            "claimedAmount": money,
            "validatedAmount": money,
            "claimed_amount": money,
            "validated_amount": money,
        })

    contest_payload = {
        **dict(normalized_args),
        "msisdn": msisdn,
        "social_sec_no": normalized_args.get("social_sec_no") or _digits_only(customer.get("document") or customer.get("socialSecNo")) or "",
        "customer_id": args.get("customer_id") or customer.get("customerId") or customer.get("id") or "",
        "current_invoice_number": args.get("current_invoice_number") or invoice.get("invoiceId") or invoice.get("invoiceNumber") or "",
        "invoice_id": args.get("invoice_id") or invoice.get("invoiceId") or "",
        "current_invoice_due_date": args.get("current_invoice_due_date") or invoice.get("dueDate") or "",
        "complete_invoices_payload": invoices,
        "servico": items[0]["itemName"] if items else str(args.get("subject") or ""),
        "valor": args.get("valor") if args.get("valor") is not None else total,
        "items": items,
        "item_msisdn": msisdn,
        "dependent_invoice_item": any(str(x.get("msisdn") or msisdn) != msisdn for x in candidates),
    }
    contest_result = await get_workflow_runtime().arun("contestacao_tool", contest_payload)
    contest_data = _workflow_result_dict(contest_result)
    contest_node_raw = _node_output(contest_result, "abrir_contestacao_cliente")
    protocol_node = _node_output(contest_result, "registrar_protocolo")
    cancellation_protocols = [
        str(row.get("protocolo_id") or row.get("protocol") or "").strip()
        for row in (cancel_node.get("protocolos_por_linha") or [])
        if isinstance(row, dict) and str(row.get("protocolo_id") or row.get("protocol") or "").strip()
    ]
    partial_protocol = str(
        protocol_node.get("protocolo_id") or protocol_node.get("protocol_number")
        or contest_data.get("protocol_number") or ""
    ).strip()
    if contest_data.get("status") != "COMPLETED" or (contest_node_raw and contest_node_raw.get("success") is False):
        details = contest_data.get("error_details") if isinstance(contest_data.get("error_details"), dict) else {}
        provider_body = details.get("body") if isinstance(details, dict) else None
        provider_text = str(provider_body or "")
        contest_error = str(
            contest_node_raw.get("guardrail_reason")
            or contest_node_raw.get("contestation_error_description")
            or contest_data.get("contestation_error_description")
            or contest_data.get("error")
            or "Contestação não executada"
        )
        normalized_error = f"{contest_error} {provider_text}".casefold()
        already_contested = any(token in normalized_error for token in (
            "já contestado", "ja contestado", "itens já contestados",
            "itens ja contestados", "item_ja_contestado", "conflito de itens",
        ))
        response_protocols = list(dict.fromkeys([*cancellation_protocols, *([partial_protocol] if partial_protocol else [])]))
        if already_contested and successful:
            message = compose_vas_cancellation_message({
                "success": True,
                "cancelados": [{"servico": x.get("subject")} for x in successful],
                "contestation_error_description": contest_error,
                "contestacao_protocol": partial_protocol,
                "protocols_for_response": response_protocols,
            })
            return {
                **cancel_data,
                "status": "COMPLETED",
                "success": True,
                "error": None,
                "mensagem": message,
                "erro_sistemico": False,
                "auto_finalize_on_failure": False,
                "protocol_closed": False,
                "contestacao_protocol": partial_protocol or None,
                "cancelamento_vas_protocol": cancellation_protocols[0] if cancellation_protocols else None,
                "protocols_for_response": response_protocols,
                "requires_protocol_in_response": bool(response_protocols),
                "contestation_error_description": contest_error,
                "contestacao_workflow": contest_data,
                "metadata": {
                    "workflow_name": "cancelamento_vas_avulso",
                    "workflow_execution_id": cancel_data.get("execution_id"),
                    "workflow_status": "COMPLETED",
                    "composite_workflows": ["cancelamento_vas_avulso", "contestacao_tool"],
                },
            }
        return {
            **cancel_data,
            "status": contest_data.get("status") or "FAILED",
            "error": contest_error,
            "success": False,
            "mensagem": contest_error,
            "erro_sistemico": True,
            "auto_finalize_on_failure": True,
            "contestacao_protocol": partial_protocol or None,
            "cancelamento_vas_protocol": cancellation_protocols[0] if cancellation_protocols else None,
            "protocols_for_response": response_protocols,
            "requires_protocol_in_response": bool(response_protocols),
            "contestation_error_description": contest_node_raw.get("contestation_error_description") or contest_data.get("contestation_error_description") or None,
            "contestacao_workflow": contest_data,
            "metadata": {
                "workflow_name": "cancelamento_vas_avulso",
                "workflow_execution_id": cancel_data.get("execution_id"),
                "workflow_status": contest_data.get("status"),
                "composite_workflows": ["cancelamento_vas_avulso", "contestacao_tool"],
            },
        }

    contest_node = _node_output(contest_result, "abrir_contestacao_cliente")
    sms_node = _node_output(contest_result, "enviar_sms")
    protocol = str(protocol_node.get("protocolo_id") or protocol_node.get("protocol_number") or "")
    response_protocols = list(dict.fromkeys([*cancellation_protocols, *([protocol] if protocol else [])]))
    items_response = contest_node.get("items_response") if isinstance(contest_node.get("items_response"), list) else []
    normalized_contested = contest_node.get("contested_items")
    normalized_not_contested = contest_node.get("not_contested_items")
    contested_items = [x for x in normalized_contested if isinstance(x, dict)] if isinstance(normalized_contested, list) else [x for x in items_response if isinstance(x, dict) and str(x.get("status") or "").upper() in {"CRIAR", "INICIADA", "ENVIADA", "SUCCESS", "SUCESSO"}]
    not_contested_items = [x for x in normalized_not_contested if isinstance(x, dict)] if isinstance(normalized_not_contested, list) else [x for x in items_response if isinstance(x, dict) and x not in contested_items]
    already_contested = contest_node.get("itens_ja_contestados")
    if not isinstance(already_contested, list):
        already_contested = contest_node.get("already_contested_items")
    if not isinstance(already_contested, list):
        already_contested = contest_data.get("itens_ja_contestados")
    if not isinstance(already_contested, list):
        already_contested = []
    # Ausência de itemsResponse só implica usar os itens solicitados como
    # contestados quando o provider também não informou explicitamente que eles
    # já haviam sido contestados. Essa distinção evita afirmar um novo crédito
    # quando a operação foi recusada por conflito de negócio.
    if not items_response and not already_contested:
        contested_items = items

    message = compose_vas_cancellation_message({
        "success": True,
        "cancelados": [{"servico": x.get("subject")} for x in successful],
        "nao_encontrados": [{"servico": x.get("subject")} for x in failed if "não encontrado" in str(x.get("error") or "").casefold()],
        "contested_items": contested_items,
        "not_contested_items": not_contested_items,
        "itens_ja_contestados": already_contested,
        "contested_invoice_amount_open": _money_ptbr(_nonzero_money_or(contest_node.get("contested_invoice_amount_open"), total)),
        "sms_sent": bool(sms_node),
        "sms_not_send_error": bool(sms_node and not sms_node.get("success", True)),
        "contestacao_protocol": protocol,
        "contestation_error_description": contest_node.get("contestation_error_description") or "",
        "protocols_for_response": response_protocols,
    })

    combined_output = dict(cancel_data.get("output") or {})
    combined_output["contestacao_tool"] = contest_data.get("output") or {}
    combined_output["resposta_cancelamento"] = {"mensagem": message}
    return {
        **cancel_data,
        "output": combined_output,
        "contestacao_workflow": contest_data,
        "success": bool(successful) or bool(contested_items),
        "contestacao_protocol": protocol,
        "cancelamento_vas_protocol": cancellation_protocols[0] if cancellation_protocols else None,
        "protocols_for_response": response_protocols,
        "requires_protocol_in_response": bool(response_protocols),
        "recomenda_finalizacao": bool(successful) or bool(contested_items),
        "status_finalizacao_sugerido": "resolvido" if (successful or contested_items) else "nao_resolvido",
        "protocol_closed": bool(contest_node.get("protocol_closed") or protocol_node.get("protocol_closed")),
        "auto_finalize_on_failure": False,
        "sms_not_send_error": bool(sms_node and not sms_node.get("success", True)),
        "contestation_error_description": contest_node.get("contestation_error_description") or "",
        "mensagem": message,
        "metadata": {
            "workflow_name": "cancelamento_vas_avulso",
            "workflow_execution_id": cancel_data.get("execution_id"),
            "workflow_status": "COMPLETED",
            "composite_workflows": ["cancelamento_vas_avulso", "contestacao_tool"],
        },
    }

async def _invoke(name: str, args: dict[str, Any]) -> Any:
    requirements = {
        "consultar_faturas": ("msisdn",),
        "invoice_explanation": ("msisdn",),
        "consultar_vas": ("msisdn",),
        "consultar_historico_vas": ("msisdn",),
        "cancelar_vas_avulso": ("msisdn", "subject"),
        "tratar_vas_estrategico": ("msisdn", "subject"),
        "contestar_cobranca": ("msisdn", "subject", "valor"),
        "pro_rata": ("msisdn",),
        "termino_desconto": ("msisdn",),
        "valor_divergente": ("msisdn",),
        "enviar_sms": ("msisdn", "message"),
        "recuperar_fatura_pdf": ("msisdn", "invoice_id"),
        "retomar_workflow": ("workflow_name", "execution_id", "resposta_usuario"),
    }
    _require(args, *requirements.get(name, ()))

    await _enrich_invoice_context(name, args)
    preflight = _preflight_subject(name, args)
    if preflight is not None:
        return _with_prefetch_events(preflight, args)
    domain_redirect = str(args.pop("_domain_redirect", "") or "")
    if domain_redirect == "tratar_vas_estrategico":
        redirected = await _run_workflow("vas_estrategico", args)
        redirected.setdefault("metadata", {})["domain_redirect_from"] = name
        redirected["metadata"]["domain_redirect_to"] = domain_redirect
        return _with_prefetch_events(redirected, args)

    workflow_map = {
        "invoice_explanation": "invoice_explanation",
        "cancelar_vas_avulso": "cancelamento_vas_avulso",
        "tratar_vas_estrategico": "vas_estrategico",
        "contestar_cobranca": "contestacao_tool",
        "pro_rata": "pro_rata",
        "termino_desconto": "termino_desconto",
        "valor_divergente": "valor_divergente",
        "finalizar_atendimento": "finalizar_atendimento",
    }
    if name == "cancelar_vas_avulso":
        return _with_prefetch_events(await _run_cancelamento_com_contestacao(args), args)
    if name in workflow_map:
        return await _run_workflow(workflow_map[name], args)
    if name == "retomar_workflow":
        result = await get_workflow_runtime().aresume(
            str(args["workflow_name"]),
            str(args["execution_id"]),
            {"resposta_usuario": args["resposta_usuario"]},
        )
        return _with_prefetch_events(_result_payload(result, workflow_name=str(args["workflow_name"])), args)

    fn: Callable[..., Any] = getattr(service, name)
    call_args = {k: v for k, v in args.items() if not str(k).startswith("_")}
    return _with_prefetch_events(fn(**call_args), args)


@app.get("/health")
async def health() -> dict[str, Any]:
    return {
        "status": "ok",
        "architecture": "framework-native",
        "legacy_dependency": False,
        "workflow_engine": "agent_framework.workflows.WorkflowRuntime",
        "langgraph_direct_import": False,
        "checkpoint_provider": getattr(settings, "CHECKPOINT_REPOSITORY_PROVIDER", "memory"),
        "gateway_mode": "mock" if service.client.mock else "real",
        "tools": len(TOOLS),
        "env_file": str(PROJECT_ROOT / ".env"),
    }


@app.get("/mcp/tools/list")
async def list_tools() -> dict[str, Any]:
    return {"tools": [{"name": name, **definition} for name, definition in TOOLS.items()]}


@app.post("/mcp/tools/call")
async def call_tool(call: ToolCall) -> dict[str, Any]:
    if call.tool_name not in TOOLS:
        return {"ok": False, "error": f"Tool não encontrada: {call.tool_name}"}
    try:
        result = await _invoke(call.tool_name, dict(call.arguments or {}))
        failed = isinstance(result, dict) and (result.get("success") is False or result.get("status") == "FAILED")
        metadata = {"server": "contas", "tool": call.tool_name, "framework_native": True}
        if isinstance(result, dict) and isinstance(result.get("metadata"), dict):
            metadata.update(result["metadata"])
        if failed:
            return {"ok": False, "error": result.get("error") or "Falha de domínio", "result": result, "metadata": metadata}
        return {"ok": True, "result": result, "metadata": metadata}
    except Exception as exc:
        return {"ok": False, "error": f"{type(exc).__name__}: {exc}", "metadata": {"server": "contas", "tool": call.tool_name, "framework_native": True}}
