"""Envio dos avisos de insolvência.

Separado do sweep de propósito: o sweep grava os hits e segue; o envio corre
depois, sobre os hits por notificar. Assim um SMTP em baixo nunca faz perder
anúncios, e um reenvio é só voltar a correr isto.

Destinatários vêm de `alerts_config` com `kind='insolvency'` — configuráveis nas
Definições, com `geral@segunor.pt` semeado por defeito.
"""
import logging
from datetime import date
from typing import Any

from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.services.alerts import send_email_raw, send_webhook_alert
from app.services.insolvency_matching import mark_notified, pending_notifications

logger = logging.getLogger(__name__)

SUBJECT = "[Intel Grid] Insolvência: {nome} — reclamar créditos até {prazo}"

BODY = """{nome} (NIF {nif}) foi declarada insolvente.

O prazo para reclamar créditos termina a {prazo}{estimado}.
Faltam {dias} dias.

Processo:      {processo}
Tribunal:      {tribunal}
{sentenca}
Administrador de insolvência (é a quem se reclama o crédito):
  {admin}
  {admin_email}

{publicacoes}
Detalhe em http://10.0.30.108/insolvencies
"""


async def _recipients(session: AsyncSession, kind: str = "insolvency") -> list[dict[str, Any]]:
    rows = (
        await session.execute(
            text(
                """
                SELECT id::text, type, target FROM alerts_config
                WHERE enabled = TRUE AND kind = :kind
                """
            ),
            {"kind": kind},
        )
    ).mappings().all()
    return [dict(r) for r in rows]


def _group_key(hit: dict[str, Any]) -> tuple[str, str]:
    return (hit.get("matched_nif") or "", hit.get("process_number") or hit["id"])


def _format(group: list[dict[str, Any]]) -> tuple[str, str]:
    """Um email por (entidade, processo), não por publicação.

    A mesma insolvência costuma render duas publicações — a sentença e o anúncio
    — e os prazos calculados diferem, porque só uma delas menciona os éditos.
    Mandar dois emails com datas diferentes sobre o mesmo processo é pior do que
    não mandar nenhum: quem lê fica sem saber qual vale. Mostramos **a data mais
    curta** como a que conta, e listamos as publicações por trás para a decisão
    poder ser confirmada na fonte.
    """
    with_deadline = [h for h in group if h.get("claim_deadline")]
    lead = min(with_deadline, key=lambda h: h["claim_deadline"]) if with_deadline else group[0]

    deadline = lead.get("claim_deadline")
    dias = (deadline - date.today()).days if deadline else "?"
    estimado = (
        " (estimado — o documento não indicava o prazo, usou-se o supletivo de"
        " 30 dias do art. 128º CIRE)"
        if lead.get("claim_deadline_estimated")
        else ""
    )
    sentenca = (
        f"Sentença:      {lead['insolvency_date']}\n" if lead.get("insolvency_date") else ""
    )

    # o administrador pode vir só numa das publicações
    admin = next((h["admin_name"] for h in group if h.get("admin_name")), None)
    admin_email = next((h["admin_email"] for h in group if h.get("admin_email")), None)

    linhas = ["Publicações que fundamentam este prazo:"]
    for h in sorted(group, key=lambda x: (x.get("claim_deadline") or date.max)):
        prazo_h = h["claim_deadline"].isoformat() if h.get("claim_deadline") else "sem prazo"
        linhas.append(f"  - {h.get('pub_date')} · {h.get('ato')} -> {prazo_h}")
        if h.get("claim_days_source"):
            linhas.append(f"      \"{h['claim_days_source']}\"")
    if len(group) > 1:
        linhas.append(
            "  (datas diferentes porque só uma das publicações refere os éditos;"
            " assume-se a mais curta)"
        )

    payload = {
        "nome": lead.get("matched_name") or lead.get("matched_nif"),
        "nif": lead.get("matched_nif"),
        "prazo": deadline.isoformat() if deadline else "por determinar",
        "estimado": estimado,
        "dias": dias,
        "processo": lead.get("process_number") or "—",
        "tribunal": lead.get("tribunal") or "—",
        "sentenca": sentenca,
        "admin": admin or "(não identificado nas publicações)",
        "admin_email": admin_email or "(as publicações não indicam email)",
        "publicacoes": "\n".join(linhas) + "\n",
    }
    return SUBJECT.format(**payload), BODY.format(**payload)


async def dispatch_pending(session: AsyncSession, limit: int = 100) -> int:
    """Envia os avisos por enviar. Devolve quantos hits foram notificados."""
    configs = await _recipients(session)
    hits = await pending_notifications(session, limit=limit)
    if not hits:
        return 0
    if not configs:
        logger.warning(
            "%d hits de insolvência por notificar mas não há destinatários activos", len(hits)
        )
        return 0

    groups: dict[tuple[str, str], list[dict[str, Any]]] = {}
    for hit in hits:
        groups.setdefault(_group_key(hit), []).append(hit)

    sent = 0
    for group in groups.values():
        subject, body = _format(group)
        delivered = False
        for cfg in configs:
            if cfg["type"] == "email":
                ok, resp = await send_email_raw(cfg["target"], subject, body)
            elif cfg["type"] == "webhook":
                ok, resp = await send_webhook_alert(cfg["target"], {"hits": group})
            else:
                ok, resp = False, f"tipo desconhecido {cfg['type']}"
            delivered = delivered or ok
            try:
                await session.execute(
                    text(
                        """
                        INSERT INTO alerts_log (announcement_id, alert_config_id, status, response)
                        VALUES (CAST(:aid AS UUID), CAST(:cid AS UUID), :st, :resp)
                        """
                    ),
                    {
                        "aid": group[0]["announcement_id"],
                        "cid": cfg["id"],
                        "st": "sent" if ok else "failed",
                        "resp": (resp or "")[:2000] or None,
                    },
                )
            except Exception as e:  # noqa: BLE001
                logger.warning("alerts_log insert falhou: %s", e)

        # Só marcamos como notificado se pelo menos um canal entregou; caso
        # contrário fica pendente e tenta outra vez na corrida seguinte.
        if delivered:
            for hit in group:
                await mark_notified(session, hit["id"])
            sent += 1

    await session.commit()
    logger.info(
        "avisos de insolvência: %d avisos entregues (%d hits agrupados)", sent, len(hits)
    )
    return sent
