"""Orquestrador do Projeto de Agendamento (Freshdesk -> Gemini -> SSW -> WhatsApp).

Ponto de entrada UNICO que o Make chama ao receber o gatilho:
    POST /agendamento/{ticket_id}

O Make so dispara com o numero do ticket; este endpoint executa todo o fluxo
no VPS e devolve o resultado. As etapas crescem aqui, uma por vez:

    Etapa 2 (ATUAL) -> busca o ticket no Freshdesk e normaliza os dados.
    Etapa 3 (TODO)  -> Gemini analisa se e' de fato pedido de agendamento.
    Etapa 4 (TODO)  -> extrai/valida o que o SSW exige p/ agendar (regras).
    Etapa 5 (TODO)  -> confirma o agendamento no WhatsApp do suporte responsavel.

Reusa o cliente Freshdesk (Freshdesk.py) -- este modulo nao fala HTTP com o
Freshdesk direto, so orquestra. Exposto como APIRouter p/ o main.py agregar.
"""
import json
import os
import re

import requests
from fastapi import APIRouter, BackgroundTasks, HTTPException

import agenda_aeroflex
import DeParaAtendentes
import registro_agendamento
import whatsapp_alertas
from Freshdesk import _resolver_credenciais, obter_ticket
from GravarAgendamento import DadosAgendamento, _data_ddmmaa, gravar_agendamento
from GravarOcorrencia import DadosOcorrencia, consultar_cnpj_pagador
from log_uso import registrar_evento

router = APIRouter()

# Parsers por cliente: alguns clientes mandam a agenda em template fixo (nao em
# tabela livre) e com semantica invertida -- notificacao de agendamento ja feito
# em vez de pedido. Cada parser expoe detectar(ticket)->bool e extrair(ticket)->
# list (mesmo formato do path generico). Quando um casa, o fluxo pula o
# classificador/extracao do Gemini e usa o parser deterministico. Ordem importa:
# o primeiro que detectar vence.
PARSERS_CLIENTE = [agenda_aeroflex]


def _detectar_cliente(ticket: dict):
    """Retorna o parser de cliente cujo detectar() casa com o ticket, ou None
    (segue o path generico do Gemini)."""
    for parser in PARSERS_CLIENTE:
        if parser.detectar(ticket):
            return parser
    return None

# A ocorrencia 15 (agendamento) NAO grava pelo painel comum de ocorrencias -- o
# SSW recusa e manda usar a Opcao 015 (tela ssw1605). Por isso o fluxo grava via
# GravarAgendamento.gravar_agendamento, nao mais via /ocorrencia codigo 15.
TEXTO_OCORRENCIA = "Agendamento solicitado pelo cliente"

# Contato exigido pela tela 015 (f5 nome / f6 telefone, ambos OBRIGATORIOS). O
# Freshdesk nao traz telefone -> vem do .env. Nome cai pro solicitante do ticket
# se AGENDAMENTO_CONTATO_NOME estiver vazio.
CONTATO_NOME_PADRAO = os.getenv("AGENDAMENTO_CONTATO_NOME", "")
CONTATO_FONE_PADRAO = os.getenv("AGENDAMENTO_CONTATO_FONE", "")
# Fictício: a Opcao 015 exige telefone. Quando o e-mail nao traz e o .env esta
# vazio, gravamos este numero em vez de barrar o ticket (regra 2026-09-02).
CONTATO_FONE_FICTICIO = os.getenv("AGENDAMENTO_CONTATO_FONE_FICTICIO", "11999999999")

# Mapas de enum do Freshdesk (numerico -> rotulo) p/ o payload sair legivel.
STATUS = {2: "Open", 3: "Pending", 4: "Resolved", 5: "Closed"}
PRIORIDADE = {1: "Low", 2: "Medium", 3: "High", 4: "Urgent"}
ORIGEM = {1: "Email", 2: "Portal", 3: "Phone", 7: "Chat", 9: "Feedback", 10: "Outbound"}

# --- Gemini (etapa 3): classifica se o ticket e' pedido de agendamento --------
# Reusa a config do REST do Google (mesma chave/modelo de ExtrairNfGemini.py).
GEMINI_MODELO = os.getenv("GEMINI_MODELO", "gemini-2.5-flash")
GEMINI_URL = (
    "https://generativelanguage.googleapis.com/v1beta/models/"
    "{modelo}:generateContent"
)
GEMINI_TIMEOUT_S = 60

# Schema que forca o Gemini a devolver so o veredito (sem extrair dados ainda --
# extracao e' etapa 4). confianca 0-1; motivo curto p/ ir pro log.
_SCHEMA_CLASSIF = {
    "type": "OBJECT",
    "properties": {
        "e_agendamento": {"type": "BOOLEAN"},
        "confianca": {"type": "NUMBER"},
        "motivo": {"type": "STRING"},
    },
    "required": ["e_agendamento", "confianca", "motivo"],
}

_PROMPT_CLASSIF = (
    "Voce e' um classificador do suporte de uma transportadora. Leia o e-mail "
    "de um ticket e decida se ele traz uma PROGRAMACAO/PEDIDO DE AGENDAMENTO de "
    "coleta/entrega de carga. "
    "IMPORTANTE: o cliente NAO precisa usar verbo explicito ('solicito', 'por "
    "favor agendar'). Basta o e-mail TRAZER a programacao das cargas -- tipico "
    "quando ha uma TABELA/LISTA com colunas como OV, Pedido, Nota Fiscal, "
    "Cidade, Data Agenda, Hora, Senha. Nesse caso e_agendamento=true. "
    "Assuntos como 'Agendas', 'Programacao de agendas' ou 'Agendas regionais' "
    "acompanhados dessa tabela SAO pedidos de agendamento (a palavra 'agenda' "
    "aqui significa a carga a ser agendada, NAO um calendario/compromisso). "
    "So marque e_agendamento=false quando NAO houver nenhuma programacao de "
    "carga: duvida, reclamacao, cobranca, resposta 'ok/obrigado', confirmacao "
    "automatica ou assunto totalmente diverso. "
    "confianca: 0.0 a 1.0. motivo: 1 frase curta em portugues explicando.\n\n"
    "ASSUNTO: {assunto}\n\nCORPO:\n{corpo}"
)


# Schema da EXTRACAO (etapa 4): 1 objeto por linha da tabela de agenda do e-mail.
# serie_nf vem da coluna "Serie" (opcional -- nem toda NF tem).
_SCHEMA_EXTRACAO = {
    "type": "OBJECT",
    "properties": {
        "agendamentos": {
            "type": "ARRAY",
            "items": {
                "type": "OBJECT",
                "properties": {
                    "nota_fiscal": {"type": "STRING"},
                    "serie_nf": {"type": "STRING"},
                    "ov": {"type": "STRING"},
                    "pedido": {"type": "STRING"},
                    "cidade": {"type": "STRING"},
                    "data_agenda": {"type": "STRING"},
                    "hora": {"type": "STRING"},
                    "senha": {"type": "STRING"},
                    "reagendamento": {"type": "STRING"},
                },
                "required": ["nota_fiscal"],
            },
        },
    },
    "required": ["agendamentos"],
}

# Schema/prompt do CONTATO (tela 015 exige nome + telefone). Vem do e-mail:
# remetente/assinatura/rodape. telefone "" quando o e-mail nao traz (o fluxo cai
# pro fallback do .env). NAO inventar numero.
_SCHEMA_CONTATO = {
    "type": "OBJECT",
    "properties": {
        "contato_nome": {"type": "STRING"},
        "contato_telefone": {"type": "STRING"},
    },
    "required": ["contato_nome", "contato_telefone"],
}

_PROMPT_CONTATO = (
    "Extraia o NOME e o TELEFONE de contato do REMETENTE deste e-mail (quem "
    "pediu o agendamento) -- olhe saudacao, assinatura e rodape. "
    "contato_telefone: SO digitos de um telefone/celular de verdade (DDD+numero). "
    "NAO confunda com senha de portaria, numero de nota, pedido, OV ou CNPJ -- se "
    "nao houver um telefone claro, devolva \"\". NAO invente. "
    "contato_nome: nome da pessoa; \"\" se nao houver.\n\n"
    "ASSUNTO: {assunto}\n\nCORPO:\n{corpo}"
)

_PROMPT_EXTRACAO = (
    "Extraia a tabela de agendamentos deste e-mail de uma transportadora. "
    "Cada LINHA da tabela e' 1 agendamento. Para cada linha devolva os campos. "
    "'nota_fiscal' = coluna Nota Fiscal (o numero). "
    "'serie_nf' = coluna Serie (deixe \"\" se a linha nao tiver serie). "
    "data_agenda no formato original; senha \"\" se 'SEM SENHA'. "
    "NAO invente linhas nem campos ausentes.\n\nCORPO:\n{corpo}"
)


def _chamar_gemini(prompt: str, schema: dict) -> dict:
    """POST generateContent com saida JSON forcada pelo schema. Devolve o dict
    ja parseado. Reusa chave/modelo do .env (mesmo REST de ExtrairNfGemini.py)."""
    api_key = os.getenv("GEMINI_API_KEY", "")
    if not api_key:
        raise HTTPException(status_code=500, detail="GEMINI_API_KEY ausente no .env.")
    payload = {
        "contents": [{"parts": [{"text": prompt}]}],
        "generationConfig": {
            "response_mime_type": "application/json",
            "response_schema": schema,
            "temperature": 0.0,
        },
    }
    resp = requests.post(
        GEMINI_URL.format(modelo=GEMINI_MODELO),
        params={"key": api_key},
        headers={"Content-Type": "application/json"},
        json=payload,
        timeout=GEMINI_TIMEOUT_S,
    )
    if resp.status_code in (401, 403):
        raise HTTPException(status_code=502, detail=f"Gemini rejeitou a chave (status {resp.status_code}).")
    if resp.status_code >= 300:
        raise HTTPException(status_code=502, detail=f"Gemini status {resp.status_code}: {resp.text[:300]}")
    try:
        texto = resp.json()["candidates"][0]["content"]["parts"][0]["text"]
        return json.loads(texto)
    except (KeyError, IndexError, TypeError, ValueError) as exc:
        raise HTTPException(status_code=502, detail=f"Gemini resposta inesperada: {exc}")


def extrair_agendamentos(corpo: str) -> list:
    """Extrai a lista de agendamentos (1 por NF) do corpo do e-mail."""
    dados = _chamar_gemini(_PROMPT_EXTRACAO.format(corpo=corpo[:12000]), _SCHEMA_EXTRACAO)
    linhas = dados.get("agendamentos") or []
    # So mantem linhas com NF (sem NF nao da p/ localizar o CTRC no SSW).
    return [ln for ln in linhas if str(ln.get("nota_fiscal") or "").strip()]


def extrair_contato(assunto: str, corpo: str) -> dict:
    """Extrai {nome, telefone} de contato do e-mail (assinatura/rodape). Campos
    vem "" quando o e-mail nao traz -- o caller aplica o fallback do .env.
    telefone volta so-digitos."""
    dados = _chamar_gemini(
        _PROMPT_CONTATO.format(assunto=assunto or "(sem assunto)", corpo=corpo[:12000]),
        _SCHEMA_CONTATO,
    )
    return {
        "nome": str(dados.get("contato_nome") or "").strip(),
        "telefone": re.sub(r"\D", "", str(dados.get("contato_telefone") or "")),
    }


def classificar_agendamento(assunto: str, corpo: str) -> dict:
    """Chama o Gemini p/ dizer se o ticket e' pedido de agendamento.
    Devolve {e_agendamento: bool, confianca: float, motivo: str}."""
    prompt = _PROMPT_CLASSIF.format(assunto=assunto or "(sem assunto)", corpo=corpo[:12000])
    veredito = _chamar_gemini(prompt, _SCHEMA_CLASSIF)
    return {
        "e_agendamento": bool(veredito.get("e_agendamento")),
        "confianca": float(veredito.get("confianca") or 0),
        "motivo": str(veredito.get("motivo") or "").strip(),
    }


def normalizar_ticket(t: dict) -> dict:
    """Achata o ticket do Freshdesk no que o fluxo de agendamento usa.

    `corpo` usa description_text (texto plano) -- o Gemium (etapa 3) e as regras
    SSW (etapa 4) trabalham em texto, nunca no HTML da tela."""
    req = t.get("requester") or {}
    return {
        "id": t.get("id"),
        "assunto": t.get("subject") or "",
        "tipo": t.get("type"),
        "status": STATUS.get(t.get("status"), t.get("status")),
        "prioridade": PRIORIDADE.get(t.get("priority"), t.get("priority")),
        "origem": ORIGEM.get(t.get("source"), t.get("source")),
        "solicitante_nome": req.get("name"),
        "solicitante_email": req.get("email") or t.get("requester_id"),
        "cc_emails": t.get("cc_emails") or [],
        "criado_em": t.get("created_at"),
        "atualizado_em": t.get("updated_at"),
        "corpo": (t.get("description_text") or "").strip(),
        "corpo_html": t.get("description") or "",
    }


def processar_agendamento(ticket_id: int) -> dict:
    """Fluxo completo do agendamento. Hoje: etapa 2 (busca + normaliza).
    Cada etapa nova encaixa aqui, preservando o retorno anterior."""
    cred = _resolver_credenciais()

    # Etapa 2: busca o ticket que o Make mandou. include=requester traz nome/email
    # do solicitante (o CNPJ/cliente sai daqui p/ achar o atendente na etapa 5).
    bruto = obter_ticket(cred, ticket_id, include="requester")
    ticket = normalizar_ticket(bruto)

    if not ticket["corpo"]:
        raise HTTPException(
            status_code=422,
            detail=f"Ticket {ticket_id} sem corpo de texto p/ analisar.",
        )

    # Etapa 3: decidir se e' agendamento + extrair as linhas.
    # Clientes com template fixo (ex: Aeroflex/Datafrete) sao roteados p/ um
    # parser deterministico -- pulam o Gemini (o classificador generico busca
    # *pedido* de agendamento e reprovaria a *notificacao* deles).
    parser = _detectar_cliente(ticket)
    if parser is not None:
        agendamentos = parser.extrair(ticket)
        if not agendamentos:
            # Casou o cliente mas nao ha agendamento valido (ex: cancelamento).
            registrar_evento(
                f"ticket={ticket_id} | {ticket['solicitante_email']} | "
                f"cliente={parser.NOME} | SEM-AGENDAMENTO (nao gravado)"
            )
            return {
                "ticket_id": ticket_id,
                "etapa": 3,
                "e_agendamento": False,
                "cliente": parser.NOME,
                "acao": "encerrado_sem_agendamento",
            }
        classif = {"e_agendamento": True, "confianca": 1.0,
                   "motivo": f"template {parser.NOME}"}
        # Notificacao automatica -> sem contato no corpo; vai direto ao fallback.
        contato = {"nome": "", "telefone": ""}
    else:
        classif = classificar_agendamento(ticket["assunto"], ticket["corpo"])
        if not classif["e_agendamento"]:
            # NAO e' agendamento: registra no log da API (ticket + e-mail + motivo)
            # e encerra o fluxo aqui -- nao segue p/ SSW/WhatsApp.
            registrar_evento(
                f"ticket={ticket_id} | {ticket['solicitante_email']} | NAO-AGENDAMENTO "
                f"| conf={classif['confianca']:.2f} | {classif['motivo']}"
            )
            return {
                "ticket_id": ticket_id,
                "etapa": 3,
                "e_agendamento": False,
                "confianca": classif["confianca"],
                "motivo": classif["motivo"],
                "acao": "encerrado_nao_agendamento",
            }
        contato = extrair_contato(ticket["assunto"], ticket["corpo"])

    # Etapa 4: grava o agendamento na Opcao 015 (tela ssw1605) de cada CTRC
    # (localizado pela NF). Contato (obrigatorio na 015) vem do e-mail (path
    # generico) ou cai pro .env / requester quando vazio.
    contato_nome = contato["nome"] or CONTATO_NOME_PADRAO or (ticket.get("solicitante_nome") or "").strip()
    contato_fone = contato["telefone"] or CONTATO_FONE_PADRAO
    if not contato_fone:
        # Sem telefone no e-mail nem no .env: usa fictício p/ nao barrar o ticket.
        contato_fone = CONTATO_FONE_FICTICIO
        registrar_evento(
            f"ticket={ticket_id} | sem telefone no e-mail/.env -> fictício "
            f"{contato_fone} gravado na Opcao 015"
        )
    if not contato_nome:
        raise HTTPException(
            status_code=422,
            detail=f"Ticket {ticket_id}: sem contato_nome (e-mail/.env/requester todos vazios).",
        )

    # Path parser ja trouxe `agendamentos`; path generico extrai agora via Gemini.
    if parser is None:
        agendamentos = extrair_agendamentos(ticket["corpo"])
        if not agendamentos:
            raise HTTPException(
                status_code=422,
                detail=f"Ticket {ticket_id}: classificado como agendamento mas sem NF extraida.",
            )

    gravacoes = _gravar_ocorrencias(ticket_id, agendamentos, contato_nome, contato_fone)

    # So notifica o atendente se ALGO novo foi agendado agora. Sem nenhuma gravacao
    # "ok" (tudo duplicado / sem data / erro) NAO manda WhatsApp -- evita ruido em
    # ticket duplicado pelo Freshdesk.
    ok = [g for g in gravacoes if g.get("status") == "ok"]
    if not ok:
        dup = sum(1 for g in gravacoes if g.get("status") == "duplicado")
        acao = "duplicado_ignorado" if dup == len(gravacoes) else "nada_gravado"
        registrar_evento(
            f"ticket={ticket_id} | {acao}: 0 agendadas agora "
            f"(dup={dup}, total={len(gravacoes)}) -> atendente nao notificado"
        )
        return {
            "ticket_id": ticket_id,
            "etapa": 4,
            "e_agendamento": True,
            "confianca": classif["confianca"],
            "qtd_nf": len(agendamentos),
            "gravacoes": gravacoes,
            "acao": acao,
        }

    # Etapa 5: confirma no WhatsApp do atendente responsavel pelo cliente.
    notificacao = _notificar_atendente(ticket_id, agendamentos, gravacoes)

    return {
        "ticket_id": ticket_id,
        "etapa": 5,
        "e_agendamento": True,
        "confianca": classif["confianca"],
        "qtd_nf": len(agendamentos),
        "qtd_agendadas": len(ok),
        "qtd_duplicadas": sum(1 for g in gravacoes if g.get("status") == "duplicado"),
        "gravacoes": gravacoes,
        "notificacao": notificacao,
    }


def _tem_data(data_ag: str) -> bool:
    """True se data_ag e' uma data real (DDMMAA/DD/MM/AAAA). Linhas 'ENTREGA
    IMEDIATA' (sem data) nao dao p/ agendar na tela 015, que exige o campo data."""
    try:
        _data_ddmmaa(data_ag)
        return True
    except HTTPException:
        return False


def _montar_observ(ag: dict, data_ag: str) -> str:
    """Observacao (f7 da tela 015): texto fixo + data + hora/senha do e-mail
    (a tela 015 nao tem campo proprio p/ hora/senha -> vao na observacao)."""
    observ = f"{TEXTO_OCORRENCIA} - {data_ag}"
    extras = []
    hora = str(ag.get("hora") or "").strip()
    senha = str(ag.get("senha") or "").strip()
    if hora and "IMEDIAT" not in hora.upper():
        extras.append(f"Hora {hora}")
    if senha and senha.upper() not in ("SEM SENHA", "NAO", "NÃO"):
        extras.append(f"Senha {senha}")
    if extras:
        observ += " (" + "; ".join(extras) + ")"
    # Observacao do proprio e-mail (ex: Aeroflex/Datafrete traz instrucao de
    # portaria). So existe no path com parser de cliente; generico nao tem.
    obs_email = str(ag.get("observacao_datafrete") or "").strip()
    if obs_email:
        observ += f" - {obs_email}"
    return observ[:500]


def _gravar_ocorrencias(ticket_id: int, agendamentos: list,
                        contato_nome: str, contato_fone: str) -> list:
    """Grava o agendamento (Opcao 015) p/ cada NF. 1 NF pode falhar (CTRC nao
    achado / SSW recusa) sem derrubar as outras -- resultado registra status por
    NF: ok | duplicado | sem_data | erro."""
    saida = []
    for ag in agendamentos:
        nf = str(ag.get("nota_fiscal") or "").strip()
        serie = str(ag.get("serie_nf") or "").strip()
        data_ag = str(ag.get("data_agenda") or "").strip()

        # Sem data real (ex: 'ENTREGA IMEDIATA') a tela 015 nao agenda -> pula.
        if not _tem_data(data_ag):
            registrar_evento(
                f"ticket={ticket_id} | NF {nf}{('-'+serie) if serie else ''} "
                f"| SEM_DATA (data='{data_ag or '-'}') -> nao agendavel na Opcao 015"
            )
            saida.append({"nota_fiscal": nf, "serie_nf": serie, "status": "sem_data",
                          "detalhe": f"data '{data_ag or '-'}' nao agendavel"})
            continue

        # Anti-duplicidade por CTRC (nao por NF): 2 NFs do MESMO CTRC (mesmo
        # embarque) ou um ticket duplicado apontam pro mesmo CTRC -> agenda 1x so.
        # A checagem roda DENTRO do gravar_agendamento, logo apos localizar o CTRC.
        marcar = lambda seq, dt, ctrc, _nf=nf, _serie=serie: registro_agendamento.registrar_ctrc(
            seq, dt, ticket_id, _nf, _serie, ctrc)

        dados = DadosAgendamento(
            nota_fiscal=nf,
            serie_nf=serie,
            data_agenda=data_ag,
            contato_nome=contato_nome,
            contato_telefone=contato_fone,
            observacao=_montar_observ(ag, data_ag),
            ctrc_mais_recente=True,  # busca por NF acha varios -> grava no mais novo
        )
        try:
            res = gravar_agendamento(
                dados,
                ja_agendado=registro_agendamento.ja_agendada_ctrc,
                marcar=marcar,
            )
            if (res or {}).get("status") == "duplicado":
                registrar_evento(
                    f"ticket={ticket_id} | NF {nf}{('-'+serie) if serie else ''} "
                    f"| CTRC {res.get('ctrc')} ja agendado {res.get('data_agenda')} -> pulado"
                )
                saida.append({"nota_fiscal": nf, "serie_nf": serie,
                              "status": "duplicado", "ssw": res})
            else:
                saida.append({"nota_fiscal": nf, "serie_nf": serie, "status": "ok", "ssw": res})
        except HTTPException as e:
            # Falha de 1 NF nao aborta o lote -- registra e segue.
            registrar_evento(
                f"ticket={ticket_id} | NF {nf}{('-'+serie) if serie else ''} | "
                f"FALHA_AGENDAMENTO {e.status_code}: {e.detail}"
            )
            saida.append({"nota_fiscal": nf, "serie_nf": serie, "status": "erro",
                          "http": e.status_code, "detalhe": str(e.detail)})
    return saida


def _montar_mensagem(ticket_id: int, cliente: str, agendamentos: list, gravacoes: list) -> str:
    """Resumo p/ o WhatsApp do atendente: confirma a gravacao + numero do ticket
    Freshdesk + 1 linha por NF (cidade/data/hora/senha + status da gravacao)."""
    # status por NF (ok/erro) a partir das gravacoes.
    st = {g["nota_fiscal"]: g for g in gravacoes}
    qtd_ok = sum(1 for g in gravacoes if g.get("status") == "ok")
    qtd_dup = sum(1 for g in gravacoes if g.get("status") == "duplicado")
    extras = []
    if qtd_dup:
        extras.append(f"{qtd_dup} ja agendada(s) antes")
    resumo = f"Agendamento (Opcao 015) gravado em {qtd_ok} NF(s)"
    if extras:
        resumo += " (" + "; ".join(extras) + ")"
    linhas = [
        "*Agendamento solicitado pelo cliente*",
        f"Ticket Freshdesk: #{ticket_id}",
        f"Cliente: {cliente or '-'}",
        resumo + ":",
    ]
    for ag in agendamentos:
        nf = str(ag.get("nota_fiscal") or "").strip()
        g = st.get(nf, {})
        status = g.get("status")
        marca = {"ok": "OK", "duplicado": "JA AGENDADA",
                 "sem_data": "SEM DATA"}.get(status, "FALHOU")
        det = ""
        if status == "ok":
            ctrc = (g.get("ssw") or {}).get("ctrc") or (g.get("ssw") or {}).get("seq_ctrc") or ""
            det = f" (CTRC {ctrc})" if ctrc else ""
        elif status in ("duplicado", "sem_data"):
            det = ""
        else:
            det = f" ({g.get('detalhe','')[:60]})"
        ctx = " ".join(p for p in (ag.get("cidade"), ag.get("data_agenda"), ag.get("hora")) if p)
        senha = ag.get("senha")
        senha_txt = f" senha {senha}" if senha else ""
        serie = ag.get("serie_nf")
        nf_txt = f"NF {nf}" + (f"/{serie}" if serie else "")
        linhas.append(f"- {nf_txt} - {ctx}{senha_txt} [{marca}]{det}")
    return "\n".join(linhas)


def _notificar_atendente(ticket_id: int, agendamentos: list, gravacoes: list) -> dict:
    """Acha o CNPJ do Pagador no SSW, resolve o atendente na planilha e manda o
    resumo no WhatsApp. Falha aqui NAO derruba o fluxo (notificacao != gravacao)."""
    # 1) CNPJ do Pagador (mesmo cliente p/ todas as NFs do ticket -> usa a 1a).
    ag0 = agendamentos[0]
    cnpj, cliente = "", ""
    try:
        info = consultar_cnpj_pagador(DadosOcorrencia(
            nota_fiscal=str(ag0.get("nota_fiscal") or "").strip(),
            serie_nf=str(ag0.get("serie_nf") or "").strip(),
            codigo_ocorrencia="15",   # read-only: consultar_cnpj_pagador ignora o codigo
            observacao="",
            ctrc_mais_recente=True,
        ))
        cnpj, cliente = info["cnpj"], info["nome"]
    except HTTPException as e:
        registrar_evento(f"ticket={ticket_id} | FALHA_CNPJ_PAGADOR {e.status_code}: {e.detail}")
        return {"status": "sem_cnpj", "detalhe": str(e.detail)}

    # 2) Atendente responsavel (planilha CNPJ -> numero).
    atendente = DeParaAtendentes.resolver(cnpj)
    numero = atendente.get("numero", "")
    if not numero:
        registrar_evento(f"ticket={ticket_id} | cliente={cliente} | CNPJ {cnpj} sem atendente na planilha")
        return {"status": "sem_atendente", "cnpj": cnpj, "cliente": cliente}

    # 3) Envia o resumo no WhatsApp (nao levanta excecao).
    msg = _montar_mensagem(ticket_id, cliente, agendamentos, gravacoes)
    ok, motivo = whatsapp_alertas.enviar(msg, numero)
    if not ok:
        registrar_evento(f"ticket={ticket_id} | WhatsApp FALHOU p/ {numero}: {motivo}")
    return {
        "status": "enviado" if ok else "falha_envio",
        "cnpj": cnpj,
        "cliente": cliente,
        "atendente": atendente.get("nome", ""),
        "numero": numero,
        "whatsapp": motivo,
    }


def _rodar_em_background(ticket_id: int):
    """Executa o fluxo e engole excecao -- BackgroundTasks nao tem quem receba o
    erro. Loga p/ registro_agendamento em vez de propagar."""
    try:
        processar_agendamento(ticket_id)
    except Exception as e:
        registrar_evento(f"ticket={ticket_id} | FALHA background: {e}")


@router.post("/agendamento/{ticket_id}", status_code=202)
def agendar(ticket_id: int, background_tasks: BackgroundTasks):
    """Ponto de entrada do Make. Recebe o numero do ticket Freshdesk e agenda o
    fluxo p/ rodar em background -- responde 202 na hora p/ nao estourar o
    timeout de 40s do Make. O resultado real vai p/ o log/WhatsApp."""
    background_tasks.add_task(_rodar_em_background, ticket_id)
    return {"status": "aceito", "ticket_id": ticket_id}
