"""Prazos de reclamação de créditos e pesquisa nos anúncios CIRE.

Duas superfícies: os *hits* (anúncios que envolvem entidades vigiadas, com o
prazo a contar) e o arquivo nacional completo, pesquisável por NIF ou nome. O
arquivo existe porque o cruzamento é retroactivo — quando um cliente entra na
watchlist queremos poder responder "e no mês passado?".
"""
from datetime import date
from typing import Annotated, Any

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.auth.deps import CurrentUser, current_user, require_admin
from app.db import get_session
from app.services.insolvency_matching import DEBTOR_ROLES, rematch_nif

router = APIRouter(prefix="/insolvencies", tags=["insolvencies"])


class WatchlistCreate(BaseModel):
    nif: str
    name: str | None = None
    notes: str | None = None


def _norm_nif(raw: str | None) -> str | None:
    """Os NIFs chegam de fontes diferentes com formatações diferentes (o
    Sabichão guarda-os com espaços, "517 607 891"). Normalizar aqui evita que a
    mesma entidade entre duas vezes na watchlist."""
    if not raw:
        return None
    digits = "".join(ch for ch in raw if ch.isdigit())
    return digits if len(digits) == 9 else None


@router.get("/deadlines")
async def list_deadlines(
    session: Annotated[AsyncSession, Depends(get_session)],
    _user: Annotated[CurrentUser, Depends(current_user)],
    include_acknowledged: bool = False,
    include_expired: bool = False,
    days_ahead: int | None = Query(default=None, ge=0, le=365),
) -> list[dict[str, Any]]:
    """Prazos que são **mesmo** uma acção da Segunor. Mais urgentes primeiro.

    O critério é estreito de propósito, e a versão anterior estava errada:

    * **Só clientes.** A Segunor factura clientes, portanto é a insolvência de um
      cliente que lhe abre um prazo para reclamar créditos. A insolvência de um
      concorrente é notícia de mercado, não tarefa — e chamar-lhe "prazo para
      reclamar créditos" com contagem decrescente é afirmar uma coisa falsa.
    * **Só quando o cliente é o insolvente.** O papel `credor` significa que a
      entidade vigiada aparece como credora no processo de um terceiro: esse prazo
      é dela e foi exercido ao ser listada. Não é prazo de ninguém aqui.

    Antes disto a página mostrava 12 "prazos activos" que eram **todos** aparições
    como credor — 10 de concorrentes e 2 de clientes —, ou seja nenhum era
    accionável. Uma página de prazos cheia de coisas que não são para fazer treina
    quem a lê a ignorá-la, o que é pior do que não a ter.

    Os hits excluídos não se apagam: continuam em `insolvency_hits` e visíveis por
    `/announcements`, que tem o arquivo nacional inteiro.
    """
    where: list[str] = [
        # cliente: pela watchlist do Sabichão ou pelo tipo de monitorização
        """(
            h.watchlist_id IS NOT NULL
            OR EXISTS (
                SELECT 1 FROM companies c
                 WHERE c.id = h.company_id AND c.monitoring_type = 'client'
            )
        )""",
        "lower(COALESCE(h.matched_role, '')) = ANY(:actionable_roles)",
    ]
    params: dict[str, Any] = {"actionable_roles": list(DEBTOR_ROLES)}
    if not include_acknowledged:
        where.append("h.acknowledged_at IS NULL")
    # Um prazo que já passou não é um prazo. Sem isto a linha ficava na página
    # para sempre a contar dias negativos — hoje não se vê porque não há nenhum
    # cliente insolvente, e é precisamente por isso que tinha de ser tratado
    # agora e não quando houver.
    if not include_expired:
        where.append("(h.claim_deadline IS NULL OR h.claim_deadline >= CURRENT_DATE)")
    if days_ahead is not None:
        where.append("h.claim_deadline <= :limit_date")
        params["limit_date"] = date.today().fromordinal(
            date.today().toordinal() + days_ahead
        )
    rows = (
        await session.execute(
            text(
                f"""
                SELECT h.id::text, h.matched_nif, h.matched_name, h.matched_role,
                       h.claim_deadline, h.acknowledged_at, h.notified_at,
                       h.company_id::text, h.watchlist_id::text,
                       (h.claim_deadline - CURRENT_DATE) AS days_left,
                       a.id::text AS announcement_id, a.process_number, a.tribunal,
                       a.juizo, a.ato, a.species, a.pub_date, a.claim_days,
                       a.claim_days_source, a.claim_deadline_estimated,
                       a.insolvency_date, a.admin_name, a.admin_email, a.admin_nif,
                       a.has_pdf, w.source AS watchlist_source
                FROM insolvency_hits h
                JOIN cire_announcements a ON a.id = h.announcement_id
                LEFT JOIN insolvency_watchlist w ON w.id = h.watchlist_id
                {f"WHERE {' AND '.join(where)}" if where else ""}
                ORDER BY h.claim_deadline NULLS LAST, h.created_at DESC
                LIMIT 500
                """
            ),
            params,
        )
    ).mappings().all()
    return [dict(r) for r in rows]


@router.post("/deadlines/{hit_id}/acknowledge", status_code=status.HTTP_204_NO_CONTENT)
async def acknowledge(
    hit_id: str,
    session: Annotated[AsyncSession, Depends(get_session)],
    user: Annotated[CurrentUser, Depends(current_user)],
) -> None:
    res = await session.execute(
        text(
            """
            UPDATE insolvency_hits
            SET acknowledged_at = now(), acknowledged_by = :uid
            WHERE id = :id AND acknowledged_at IS NULL
            """
        ),
        {"id": hit_id, "uid": user.id},
    )
    if res.rowcount == 0:
        raise HTTPException(status_code=404, detail="hit inexistente ou já tratado")
    await session.commit()


@router.delete("/deadlines/{hit_id}/acknowledge", status_code=status.HTTP_204_NO_CONTENT)
async def unacknowledge(
    hit_id: str,
    session: Annotated[AsyncSession, Depends(get_session)],
    _user: Annotated[CurrentUser, Depends(current_user)],
) -> None:
    """Reabre um prazo marcado por engano. Marcar como tratado esconde o aviso,
    por isso tem de haver caminho de volta."""
    await session.execute(
        text(
            "UPDATE insolvency_hits SET acknowledged_at = NULL, acknowledged_by = NULL"
            " WHERE id = :id"
        ),
        {"id": hit_id},
    )
    await session.commit()


@router.get("/announcements")
async def search_announcements(
    session: Annotated[AsyncSession, Depends(get_session)],
    _user: Annotated[CurrentUser, Depends(current_user)],
    q: str | None = Query(default=None, max_length=200),
    only_claims: bool = False,
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=50, ge=1, le=200),
) -> dict[str, Any]:
    """Pesquisa no arquivo nacional. `q` aceita NIF (exacto) ou nome (fuzzy).

    Devolve `{items, total, page, page_size}` como o resto das tabelas paginadas.
    Antes devolvia a lista nua com um tecto de 50, e o ecrã escrevia "50
    anúncios" — que se lia como *são* 50 quando o arquivo tem quase 19 mil e não
    havia forma nenhuma de chegar ao 51.
    """
    where: list[str] = []
    params: dict[str, Any] = {}
    if only_claims:
        where.append("a.opens_claim_window")
    if q:
        nif = _norm_nif(q)
        if nif:
            where.append(
                "EXISTS (SELECT 1 FROM cire_announcement_parties p"
                " WHERE p.announcement_id = a.id AND p.nif = :nif)"
            )
            params["nif"] = nif
        else:
            where.append(
                "EXISTS (SELECT 1 FROM cire_announcement_parties p"
                # `%` simples: com asyncpg o `%%` não colapsa e o operador
                # trigram fica literalmente `%%`, que não existe. Isto
                # rebentava a pesquisa por nome desde a Fase 7 — por NIF
                # funcionava, que é o caminho por onde se costuma entrar.
                " WHERE p.announcement_id = a.id AND p.name % :q)"
            )
            params["q"] = q
    clause = f"WHERE {' AND '.join(where)}" if where else ""
    total = (
        await session.execute(
            text(f"SELECT count(*) FROM cire_announcements a {clause}"), params
        )
    ).scalar_one()
    params["lim"] = page_size
    params["off"] = (page - 1) * page_size
    rows = (
        await session.execute(
            text(
                f"""
                SELECT a.id::text, a.referencia, a.tribunal, a.juizo, a.ato,
                       a.species, a.process_number, a.pub_date, a.claim_deadline,
                       a.claim_days, a.claim_deadline_estimated, a.opens_claim_window,
                       a.admin_name, a.admin_email, a.has_pdf,
                       (SELECT count(*) FROM cire_announcement_parties p
                         WHERE p.announcement_id = a.id) AS party_count
                FROM cire_announcements a
                {clause}
                ORDER BY a.pub_date DESC NULLS LAST, a.first_seen_at DESC
                LIMIT :lim OFFSET :off
                """
            ),
            params,
        )
    ).mappings().all()
    return {
        "items": [dict(r) for r in rows],
        "total": int(total),
        "page": page,
        "page_size": page_size,
    }


@router.get("/announcements/{announcement_id}")
async def announcement_detail(
    announcement_id: str,
    session: Annotated[AsyncSession, Depends(get_session)],
    _user: Annotated[CurrentUser, Depends(current_user)],
) -> dict[str, Any]:
    row = (
        await session.execute(
            text(
                """
                SELECT a.*, a.id::text AS id_text
                FROM cire_announcements a WHERE a.id = :id
                """
            ),
            {"id": announcement_id},
        )
    ).mappings().first()
    if not row:
        raise HTTPException(status_code=404, detail="anúncio não encontrado")
    parties = (
        await session.execute(
            text(
                """
                SELECT role, name, nif FROM cire_announcement_parties
                WHERE announcement_id = :id ORDER BY role, name
                """
            ),
            {"id": announcement_id},
        )
    ).mappings().all()
    out = {k: v for k, v in dict(row).items() if k not in ("id", "raw")}
    out["id"] = row["id_text"]
    out.pop("id_text", None)
    out["parties"] = [dict(p) for p in parties]
    return out


@router.get("/watchlist")
async def list_watchlist(
    session: Annotated[AsyncSession, Depends(get_session)],
    _user: Annotated[CurrentUser, Depends(current_user)],
) -> list[dict[str, Any]]:
    rows = (
        await session.execute(
            text(
                """
                SELECT w.id::text, w.nif, w.name, w.source, w.external_ref,
                       w.active, w.notes, w.created_at, w.updated_at,
                       (SELECT count(*) FROM insolvency_hits h WHERE h.watchlist_id = w.id) AS hits
                FROM insolvency_watchlist w
                ORDER BY w.name NULLS LAST, w.nif
                """
            )
        )
    ).mappings().all()
    return [dict(r) for r in rows]


@router.post("/watchlist", status_code=status.HTTP_201_CREATED)
async def add_watchlist(
    payload: WatchlistCreate,
    session: Annotated[AsyncSession, Depends(get_session)],
    _admin: Annotated[CurrentUser, Depends(require_admin)],
) -> dict[str, Any]:
    nif = _norm_nif(payload.nif)
    if not nif:
        raise HTTPException(status_code=400, detail="NIF inválido")
    await session.execute(
        text(
            """
            INSERT INTO insolvency_watchlist (nif, name, source, notes)
            VALUES (:nif, :name, 'manual', :notes)
            ON CONFLICT (nif) DO UPDATE
                SET name = COALESCE(EXCLUDED.name, insolvency_watchlist.name),
                    notes = COALESCE(EXCLUDED.notes, insolvency_watchlist.notes),
                    active = true,
                    updated_at = now()
            """
        ),
        {"nif": nif, "name": payload.name, "notes": payload.notes},
    )
    # cruzar já contra o histórico: é meio caminho do valor de guardar tudo
    hits = await rematch_nif(session, nif)
    await session.commit()
    return {"nif": nif, "hits_encontrados": hits}


@router.patch("/watchlist/{watchlist_id}", status_code=status.HTTP_204_NO_CONTENT)
async def toggle_watchlist(
    watchlist_id: str,
    active: bool,
    session: Annotated[AsyncSession, Depends(get_session)],
    _admin: Annotated[CurrentUser, Depends(require_admin)],
) -> None:
    """Desactiva sem apagar. Nunca removemos entradas: um cliente que deixou de
    o ser continua a ser um devedor cujo processo interessa acompanhar."""
    await session.execute(
        text(
            "UPDATE insolvency_watchlist SET active = :a, updated_at = now() WHERE id = :id"
        ),
        {"a": active, "id": watchlist_id},
    )
    await session.commit()


@router.get("/summary")
async def summary(
    session: Annotated[AsyncSession, Depends(get_session)],
    _user: Annotated[CurrentUser, Depends(current_user)],
) -> dict[str, Any]:
    """Números para o widget do dashboard.

    Conta o **mesmo** que a página de prazos: só clientes insolventes. Um número
    no dashboard tem de significar tantas coisas para fazer, senão é decoração —
    e antes disto contava 12 aparições como credor que não eram acção de ninguém.
    """
    row = (
        await session.execute(
            text(
                """
                WITH accionavel AS (
                    SELECT h.*
                      FROM insolvency_hits h
                     WHERE (
                             h.watchlist_id IS NOT NULL
                             OR EXISTS (
                                SELECT 1 FROM companies c
                                 WHERE c.id = h.company_id AND c.monitoring_type = 'client')
                           )
                       AND lower(COALESCE(h.matched_role, '')) = ANY(:roles)
                )
                SELECT
                  count(*) FILTER (
                    WHERE h.acknowledged_at IS NULL AND h.claim_deadline >= CURRENT_DATE
                      AND h.claim_deadline <= CURRENT_DATE + 7) AS a_expirar_7d,
                  count(*) FILTER (
                    WHERE h.acknowledged_at IS NULL AND h.claim_deadline >= CURRENT_DATE) AS abertos,
                  count(*) FILTER (
                    WHERE h.acknowledged_at IS NULL AND h.claim_deadline < CURRENT_DATE) AS expirados,
                  (SELECT count(*) FROM cire_announcements) AS anuncios,
                  (SELECT count(*) FROM insolvency_watchlist WHERE active) AS vigiados
                FROM accionavel h
                """
            ),
            {"roles": list(DEBTOR_ROLES)},
        )
    ).mappings().first()
    return dict(row) if row else {}
