"""SICAE — Sistema de Informação da Classificação Portuguesa de Actividades Económicas.

Fonte oficial do CAE Rev.4, gratuita e sem quota. Substitui o ptdata, que
morreu (503) e era de onde vinha o CAE.

Portado de `funcoes/empresa_nif_api.php` do Sabichão, que faz esta agregação em
produção há meses. Preservam-se duas decisões de lá que não são óbvias:

* **O NIF aparece duas vezes na resposta** — na linha de resultado e no `<input>`
  da pesquisa, porque o WebForms reescreve o valor submetido. Só contam as
  ocorrências dentro de `<td>`, senão qualquer pesquisa "encontra" o NIF.
* **Não encontrar não é erro.** É o caso normal para NIFs de pessoa singular ou
  entidades sem CAE registado. O sinal de "o site mudou" é dado noutro sítio —
  quando o formulário deixa de ter o `__VIEWSTATE` ou o campo do NIPC. Se o
  alarme fosse no parse, cada "não encontrado" seria um falso alarme e afogava
  o sinal verdadeiro.
"""
import logging
import re
from typing import Any

from app.scrapers.base import build_client

logger = logging.getLogger(__name__)

BASE = "http://www.sicae.pt/"
CONSULTA = BASE + "Consulta.aspx"

F_NIPC = "ctl00$MainContent$ipNipc"
F_FIRMA = "ctl00$MainContent$ipFirma"
F_CAE = "ctl00$MainContent$ipCae"
F_BTN = "ctl00$MainContent$btnPesquisa"

_ROW_RE = re.compile(r"<tr[^>]*>(.*?)</tr>", re.I | re.S)
_CELL_RE = re.compile(r"<td[^>]*>(.*?)</td>", re.I | re.S)
_TAG_RE = re.compile(r"<[^>]+>")


class SicaeFormChanged(RuntimeError):
    """O formulário do SICAE mudou; o scraping precisa de ser revisto."""


def _hidden(html: str) -> dict[str, str]:
    out: dict[str, str] = {}
    for tag in re.findall(r"<input[^>]*type=\"hidden\"[^>]*>", html, re.I):
        name = re.search(r'name="([^"]+)"', tag)
        value = re.search(r'value="([^"]*)"', tag)
        if name:
            out[name.group(1)] = value.group(1) if value else ""
    return out


def _cells(row_html: str) -> list[str]:
    import html as _html

    out = []
    for cell in _CELL_RE.findall(row_html):
        text = _TAG_RE.sub(" ", cell)
        out.append(re.sub(r"\s+", " ", _html.unescape(text)).strip())
    return out


def _parse_rows(html: str) -> list[dict[str, Any]]:
    """Linhas de resultado -> [{nipc, denominacao, cae, cae_secundarios}]."""
    out: list[dict[str, Any]] = []
    for row in _ROW_RE.findall(html):
        vals = _cells(row)
        if len(vals) < 3:
            continue
        nipc = re.sub(r"\D+", "", vals[0])
        if len(nipc) != 9:
            continue
        denom = vals[1] or None
        cae = re.sub(r"[^0-9]", "", vals[2]) or None
        secs = re.findall(r"\d{4,5}", vals[3]) if len(vals) > 3 and vals[3] else []
        if not denom and not cae:
            continue
        out.append(
            {
                "nipc": nipc,
                "denominacao": denom,
                "cae": cae,
                "cae_secundarios": ", ".join(secs) or None,
            }
        )
    return out


async def _search(nipc: str = "", firma: str = "") -> list[dict[str, Any]]:
    async with build_client(timeout=20.0) as client:
        page = await client.get(CONSULTA)
        page.raise_for_status()
        hidden = _hidden(page.text)

        # É aqui que se detecta uma alteração ao site. Logar só isto — e não o
        # parse — mantém o alarme fiável.
        if "__VIEWSTATE" not in hidden or F_NIPC not in page.text:
            logger.error("SICAE: o formulário de Consulta.aspx mudou — scraping a rever")
            raise SicaeFormChanged("formulário do SICAE alterado")

        data = {
            **hidden,
            "__EVENTTARGET": "",
            "__EVENTARGUMENT": "",
            F_NIPC: nipc,
            F_FIRMA: firma,
            F_CAE: "",
            F_BTN: "Pesquisar",
        }
        r = await client.post(
            CONSULTA,
            data=data,
            headers={"Referer": CONSULTA, "Content-Type": "application/x-www-form-urlencoded"},
        )
        r.raise_for_status()
        return _parse_rows(r.text)


async def lookup_by_nipc(nif: str) -> dict[str, Any] | None:
    """Denominação social + CAE principal e secundários. None quando não há
    registo — o que é normal, não é falha."""
    nif = re.sub(r"\D+", "", nif or "")
    if len(nif) != 9:
        return None
    try:
        rows = await _search(nipc=nif)
    except SicaeFormChanged:
        raise
    except Exception as e:  # noqa: BLE001
        logger.info("SICAE lookup %s falhou: %s", nif, e)
        return None
    return next((r for r in rows if r["nipc"] == nif), None)


async def search_by_name(name: str, limit: int = 10) -> list[dict[str, Any]]:
    """Pesquisa inversa nome -> NIPC. Útil quando uma publicação ou um anúncio
    traz uma entidade sem NIF."""
    name = (name or "").strip()
    if len(name) < 3:
        return []
    try:
        rows = await _search(firma=name)
    except SicaeFormChanged:
        raise
    except Exception as e:  # noqa: BLE001
        logger.info("SICAE pesquisa por nome %r falhou: %s", name, e)
        return []
    return rows[:limit]
