"""Extracção do prazo de reclamação de créditos a partir do PDF do anúncio CIRE.

O bloco HTML da listagem não traz o prazo — só o PDF do anúncio o diz, e diz-o
textualmente: "O prazo para a reclamação de créditos foi fixado em 30 dias."
Guardamos também a frase que originou o número, porque a data calculada é o que
faz alguém decidir se ainda vai a tempo de reclamar um crédito: quem consulta
tem de poder confirmar a origem em vez de confiar num número sem rasto.

Os PDFs vêm com quebras de linha a meio das frases (layout de duas colunas com
cabeçalho), por isso normalizamos os espaços antes de aplicar os padrões.
"""
import io
import logging
import re
from datetime import date, timedelta
from typing import Any

logger = logging.getLogger(__name__)

# Prazo legal supletivo do art. 128º CIRE, usado só quando o PDF não está
# disponível ou não tem camada de texto. Fica marcado como estimado.
DEFAULT_CLAIM_DAYS = 30

_WS_RE = re.compile(r"\s+")

# O pypdf intercala espaços dentro dos números ("identificação fiscal: 5003916 02",
# "4820 -250 Fafe"). Se exigirmos \d{9} o padrão falha nesse ponto e — pior —
# continua a procurar, apanhando o NIF da pessoa seguinte no texto. Capturamos
# dígitos com espaços pelo meio e normalizamos depois.
_NIF_CAP = r"(\d[\d ]{7,15}\d)"


def _norm_nif(raw: str | None) -> str | None:
    if not raw:
        return None
    digits = re.sub(r"\D", "", raw)
    return digits if len(digits) == 9 else None

# Os tribunais usam formulários com espaços para preencher, e o número aparece
# rodeado de underscores ou asteriscos ("fixado em __30__ dias"). E a parte
# dispositiva da sentença inverte a ordem ("Fixo em 30 dias o prazo para..."),
# ao contrário do anúncio. Ambas as formas são comuns — na primeira corrida
# real representavam a maioria dos casos que ficavam sem prazo.
_N = r"[_*\s]*(\d{1,3})[_*\s]*"

CLAIM_DAYS_RES = [
    re.compile(
        r"prazo\s+para\s+a\s+reclama[çc][ãa]o\s+de\s+cr[ée]ditos\s+"
        r"(?:foi\s+fixado\s+em|fixado\s+em|[ée]\s+de)" + _N + r"dias",
        re.IGNORECASE,
    ),
    re.compile(
        r"[Ff]ix[oa](?:-se)?\s+em" + _N + r"dias\s+o\s+prazo\s+"
        r"(?:para\s+a\s+)?reclama[çc][ãa]o\s+de\s+cr[ée]ditos",
        re.IGNORECASE,
    ),
    re.compile(
        r"reclama[çc][ãa]o\s+de\s+cr[ée]ditos.{0,60}?no\s+prazo\s+de" + _N + r"dias",
        re.IGNORECASE,
    ),
]

EDITOS_RE = re.compile(r"correm\s+[ée]ditos\s+de\s+(\d{1,3})\s+dias", re.IGNORECASE)

INSOLVENCY_DATE_RE = re.compile(
    r"no\s+dia\s+(\d{1,2})[-/](\d{1,2})[-/](\d{4})", re.IGNORECASE
)

# `[^;]` é deliberado: impede o padrão de atravessar o ponto-e-vírgula e ir
# buscar o NIF da pessoa seguinte (o administrador da devedora vem logo a
# seguir ao insolvente, e era esse que aparecia antes desta correcção).
DEBTOR_RE = re.compile(
    r"devedora?\s*/\s*insolvente\s*:\s*([^;]{3,300}?)\s*;\s*"
    r"identifica[çc][ãa]o\s+fiscal\s*:\s*" + _NIF_CAP,
    re.IGNORECASE,
)
DEBTOR_ADDR_RE = re.compile(
    r"devedora?\s*/\s*insolvente\s*:.{0,200}?com\s+domic[íi]lio\s+na\s+(.+?)(?:,\s*com\s+sede|\.\s)",
    re.IGNORECASE,
)

# O administrador de insolvência é quem recebe as reclamações de crédito, por
# isso vale a pena tolerar as variantes. Vistas em produção, todas no mesmo dia:
#   "…respetivo domicílio: Dr. Fulano; identificação fiscal: …"
#   "…respetivo domicílio. Dra. Fulana; identificação fiscal: …"   (ponto final)
#   "…é nomeado: Dr. Fulano; identificação fiscal: …"
#   "Administradora da Insolvência é nomeada: …"                   (feminino)
#   "Para Administrador de Insolvência nomeio o Sr. Dr. Fulano(Artigo 36.º…)" (sem NIF)
_ADMIN_ANCHOR = r"Administrador[a]?\s+d[ae]\s+Insolv[êe]ncia\b"
_ADMIN_INTRO = r"(?:domic[íi]lio\s*[:.]|nomead[oa]\s*:|nomeio\s+(?:o|a)\b)\s*"
_TITLES = r"(?:(?:Sr\.?a?\.?|Dr\.?a?\.?|Eng\.?a?\.?)\s*)*"

ADMIN_RES = [
    # com NIF — preferido, é o que permite ligar a pessoa a outras entidades
    re.compile(
        _ADMIN_ANCHOR + r".{0,200}?" + _ADMIN_INTRO + _TITLES +
        r"([^;(]{3,120}?)\s*(?:\(([^)]+)\))?\s*;\s*"
        r"identifica[çc][ãa]o\s+fiscal\s*:\s*" + _NIF_CAP,
        re.IGNORECASE,
    ),
    # sem NIF — alguns dispositivos de sentença só dão o nome
    re.compile(
        _ADMIN_ANCHOR + r".{0,200}?" + _ADMIN_INTRO + _TITLES +
        r"([^;(\n]{3,120}?)\s*(?:\(|;|\.\s)",
        re.IGNORECASE,
    ),
]
EMAIL_RE = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")


def pdf_to_text(data: bytes | None) -> str | None:
    """Texto do PDF, ou None se não houver camada de texto (alguns anúncios são
    digitalizações). Não fazemos OCR — nesses casos o prazo fica estimado."""
    if not data:
        return None
    try:
        from pypdf import PdfReader

        reader = PdfReader(io.BytesIO(data))
        text = "\n".join((page.extract_text() or "") for page in reader.pages)
    except Exception as e:  # noqa: BLE001 — PDF corrompido não pode parar o sweep
        logger.info("cire_announcement_parser: pypdf falhou: %s", e)
        return None
    return text if text.strip() else None


def _flat(text: str) -> str:
    return _WS_RE.sub(" ", text)


def _clean_addr(raw: str) -> str | None:
    """Cola de volta os códigos postais que o pypdf parte ("4820 -250" -> "4820-250")."""
    addr = re.sub(r"(\d{4})\s*-\s*(\d{3})", r"\1-\2", raw).strip(" ,;")
    return addr[:500] or None


def parse_announcement_text(text: str | None) -> dict[str, Any]:
    """Campos estruturados a partir do texto do PDF. Todos opcionais — os
    anúncios variam bastante de tribunal para tribunal."""
    out: dict[str, Any] = {
        "claim_days": None,
        "claim_days_source": None,
        "editos_days": None,
        "insolvency_date": None,
        "debtor_nif": None,
        "debtor_name": None,
        "debtor_address": None,
        "admin_name": None,
        "admin_email": None,
        "admin_nif": None,
    }
    if not text:
        return out
    flat = _flat(text)

    for rx in CLAIM_DAYS_RES:
        m = rx.search(flat)
        if m:
            out["claim_days"] = int(m.group(1))
            out["claim_days_source"] = m.group(0).strip()[:300]
            break

    m = EDITOS_RE.search(flat)
    if m:
        out["editos_days"] = int(m.group(1))

    m = INSOLVENCY_DATE_RE.search(flat)
    if m:
        d, mm, y = (int(x) for x in m.groups())
        try:
            out["insolvency_date"] = date(y, mm, d)
        except ValueError:
            pass

    m = DEBTOR_RE.search(flat)
    if m:
        out["debtor_name"] = m.group(1).strip()[:500]
        out["debtor_nif"] = _norm_nif(m.group(2))
    m = DEBTOR_ADDR_RE.search(flat)
    if m:
        out["debtor_address"] = _clean_addr(m.group(1))

    for rx in ADMIN_RES:
        m = rx.search(flat)
        if not m:
            continue
        groups = m.groups()
        name = (groups[0] or "").strip(" ,;.")
        if not name:
            continue
        out["admin_name"] = name[:300]
        paren = groups[1] if len(groups) > 1 else None
        if paren:
            found = EMAIL_RE.search(paren)  # o parêntesis nem sempre é o email
            out["admin_email"] = found.group(0) if found else None
        if len(groups) > 2:
            out["admin_nif"] = _norm_nif(groups[2])
        break

    return out


def compute_deadline(
    pub_date: date | None,
    claim_days: int | None,
    editos_days: int | None,
    opens_claim_window: bool,
) -> tuple[date | None, bool]:
    """(prazo, estimado). O prazo conta a partir da publicação, depois de
    correrem os éditos. Quando o PDF não deu o número usamos o supletivo de 30
    dias do art. 128º CIRE e marcamos como estimado, para a UI o poder dizer.

    Devolve (None, False) para actos que não abrem prazo nenhum."""
    if not pub_date or not opens_claim_window:
        return None, False
    estimated = claim_days is None
    days = claim_days if claim_days is not None else DEFAULT_CLAIM_DAYS
    return pub_date + timedelta(days=days + (editos_days or 0)), estimated
