"""Extract (name, nif, cargo, event) tuples from a MJ DetalhePublicacao body.

The MJ portal renders publication detail pages with a predictable block
format when natural persons are referenced:

    Nome/Firma: RICARDO JOSE PIMENTA FRADE
    NIF/NIPC: 214480917
    [Nacionalidade: ...]
    [Residência/Sede: ...]
    Cargo: gerente
    [Causa: renúncia]

The enclosing act line ("DESIGNAÇÃO DE MEMBRO(S)..." / "CESSAÇÃO DE
FUNÇÕES...") tells us whether the person is being appointed (`appointed`)
or removed (`ceased`). Anything that doesn't match those keywords goes in
as `other` — still useful for cross-company lookup even if the semantic
is unclear.

This parser is deliberately tolerant: it grabs every Nome/Firma block in
the body and pulls the next NIF + the first Cargo within ~500 chars. It
doesn't try to be correct about which organ section each person belongs
to (GERÊNCIA / CONSELHO DE ADMINISTRAÇÃO / etc.) — we store the cargo
text as-is.
"""
import re
from typing import Any

_PERSON_BLOCK = re.compile(
    r"Nome/Firma:\s*(?P<name>[^\n]+?)\s*\n"
    r"NIF/NIPC:\s*(?P<nif>\d{9})\b"
    r"(?P<trailer>.{0,500})",
    re.DOTALL | re.IGNORECASE,
)

# Shareholders ("sócios") use a different block shape — TITULAR for the
# name, with QUOTA preceding it in the SÓCIOS E QUOTAS section. We capture
# the quota amount as part of the cargo text so the UI can surface it.
_TITULAR_BLOCK = re.compile(
    r"(?:QUOTA\s*:\s*(?P<quota>[^\n]+)\s*\n)?"
    r"TITULAR:\s*(?P<name>[^\n]+?)\s*\n"
    r"NIF/NIPC:\s*(?P<nif>\d{9})\b",
    re.DOTALL | re.IGNORECASE,
)

_CARGO_RE = re.compile(r"Cargo:\s*([^\n]+)", re.IGNORECASE)
_CAUSA_RE = re.compile(r"Causa:\s*([^\n]+)", re.IGNORECASE)
_ACT_HEADER_RE = re.compile(r"UTC\s*-\s*([^\n]+)", re.IGNORECASE)

# Section headers used by MJ to group people blocks when no explicit
# "Cargo:" line is present (e.g. FISCAL ÚNICO:, GERÊNCIA:, SUPLENTE(S) DO
# FISCAL ÚNICO:). Excluded below are the metadata headers / page-level
# captions that aren't role markers.
_SKIP_HEADERS = {
    "NIF/NIPC", "FIRMA", "SEDE", "OBJECTO", "OBJETO",
    "NATUREZA JURÍDICA", "NOME/FIRMA", "TITULAR", "DATA",
    "CAUSA", "QUOTA", "CAPITAL", "ORGÃO(S) DESIGNADO(S)",
    "ÓRGÃO(S) DESIGNADO(S)", "SÓCIOS E QUOTAS",
    "FORMA DE OBRIGAR/ÓRGÃOS SOCIAIS", "DESENVOLVIMENTO",
    "PUBLICAÇÃO", "DATA PUBLICAÇÃO",
}


def _infer_cargo_from_section(body: str, pos: int) -> str | None:
    """When a Nome/Firma block has no inline "Cargo:" line, the role is
    implied by the most recent section header (uppercase label ending
    with a colon) that precedes the block. Example:

        FISCAL ÚNICO:
        Nome/Firma: …
        NIF/NIPC: …
        Causa: …

    Scan up to ~600 chars backward from the block start, find the
    closest upper-case line ending with ":" that isn't in the metadata
    skiplist, and use its label as the cargo."""
    prefix = body[max(0, pos - 600):pos]
    for line in reversed(prefix.splitlines()):
        raw = line.strip()
        if not raw.endswith(":"):
            continue
        label = raw.rstrip(":").strip()
        if not label or len(label) < 3:
            continue
        # Allow labels that are mostly uppercase. Numbers/spaces/parens OK.
        letters = [c for c in label if c.isalpha()]
        if not letters:
            continue
        upper_ratio = sum(1 for c in letters if c.isupper()) / len(letters)
        if upper_ratio < 0.8:
            continue
        # Normalise for comparison against the skiplist (strip accents-
        # differences is fine since skiplist uses same accents we emit).
        if label.upper() in _SKIP_HEADERS:
            continue
        return label
    return None


def _classify_act(body: str) -> str:
    """Look at the act title line ("... UTC - <ACT>") to decide the event.
    Only the first act header in the body is considered — MJ detail pages
    occasionally contain multiple acts, but the first is usually the
    primary one."""
    m = _ACT_HEADER_RE.search(body)
    if not m:
        return "other"
    act = m.group(1).upper()
    if "CESSAÇÃO" in act or "CESSACAO" in act:
        return "ceased"
    if any(kw in act for kw in ("DESIGNAÇÃO", "DESIGNACAO", "NOMEAÇÃO", "NOMEACAO")):
        return "appointed"
    return "other"


def extract_people(body: str | None) -> list[dict[str, Any]]:
    if not body:
        return []
    event = _classify_act(body)
    # De-dup by (nif, role_kind) so the same person can be captured as
    # BOTH gerente (Nome/Firma block) AND sócio (TITULAR block) when the
    # publication mentions them in both sections — which is the norm in
    # Constituições where founders are also the initial board.
    seen: set[tuple[str, str]] = set()
    people: list[dict[str, Any]] = []

    # 1. Management roles: Nome/Firma + Cargo (gerente, administrador, …)
    for m in _PERSON_BLOCK.finditer(body):
        name = (m.group("name") or "").strip()
        nif = (m.group("nif") or "").strip()
        trailer = m.group("trailer") or ""
        key = (nif, "admin")
        if not nif or key in seen:
            continue
        seen.add(key)
        cargo_m = _CARGO_RE.search(trailer)
        causa_m = _CAUSA_RE.search(trailer)
        cargo_val: str | None = None
        if cargo_m:
            cargo_val = cargo_m.group(1).strip()
        else:
            # Fallback to the enclosing section header (FISCAL ÚNICO,
            # SUPLENTE(S) DO FISCAL ÚNICO, CONSELHO DE ADMINISTRAÇÃO …).
            cargo_val = _infer_cargo_from_section(body, m.start())
        people.append({
            "name": name[:200],
            "nif": nif,
            "cargo": (cargo_val[:200] if cargo_val else None),
            "causa": (causa_m.group(1).strip() if causa_m else None),
            "event": event,
        })

    # 2. Shareholders: TITULAR + optional preceding QUOTA. Independent of
    # any admin role the same NIF may have held — sócio is ownership,
    # gerente is management, we track both separately.
    for m in _TITULAR_BLOCK.finditer(body):
        name = (m.group("name") or "").strip()
        nif = (m.group("nif") or "").strip()
        quota = (m.group("quota") or "").strip() if m.group("quota") else None
        key = (nif, "socio")
        if not nif or key in seen:
            continue
        seen.add(key)
        cargo = "sócio"
        if quota:
            cargo = f"sócio — {quota}"
        people.append({
            "name": name[:200],
            "nif": nif,
            "cargo": cargo[:200],
            "causa": None,
            "event": "appointed",
        })

    return people
