"""Scrape the PSP's public security-company roster (sigesponline.psp.pt).

The site is a JSF/PrimeFaces app — too stateful for a clean httpx-only
scrape (ViewState + partial-ajax + jsessionid dance returns empty
tables when done out-of-browser). We drive a real headless Chromium via
Playwright, which behaves like any user: select the Tipo de Alvará,
click Pesquisar, wait for the results table, read it.

Output: a list of dicts {nipc, nome, alvara_number, morada, localidade,
telefone, email}. Missing fields are returned as None.
"""
import logging
import re
from typing import Any

from playwright.async_api import async_playwright

logger = logging.getLogger(__name__)

PSP_URL = "https://sigesponline.psp.pt/pages/listagens/empresas_alvara/detalhe.xhtml"

# Select value → human label. The PSP page puts "1" for Alvará A, "2" for B, …
_ALVARA_VALUE = {"A": "1", "B": "2", "C": "3", "D": "4"}


async def scrape_alvara_list(alvara_type: str = "A") -> list[dict[str, Any]]:
    """Fetch the full listing for one alvará type. Returns every row in
    the results table. Raises on navigation/timeout failure."""
    if alvara_type not in _ALVARA_VALUE:
        raise ValueError(f"bad alvara_type {alvara_type!r}")
    sel_value = _ALVARA_VALUE[alvara_type]

    async with async_playwright() as pw:
        browser = await pw.chromium.launch(
            headless=True,
            args=[
                "--no-sandbox",
                "--disable-dev-shm-usage",
                "--disable-blink-features=AutomationControlled",
            ],
        )
        ctx = await browser.new_context(
            user_agent=(
                "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
                "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
            ),
            locale="pt-PT",
            viewport={"width": 1280, "height": 900},
        )
        page = await ctx.new_page()

        try:
            logger.info("PSP: loading listing page…")
            await page.goto(PSP_URL, wait_until="networkidle", timeout=45000)

            # Select the alvará type (dropdown id=selectCharterType)
            await page.select_option("#selectCharterType", sel_value)

            # Click "Pesquisar" — id j_idt31 is the PrimeFaces command button
            async with page.expect_response(
                lambda r: "detalhe.xhtml" in r.url and r.request.method == "POST",
                timeout=30000,
            ):
                await page.click("#j_idt31")

            # Results get rendered into #resultGroup. Give the AJAX time to
            # swap in the table.
            await page.wait_for_function(
                "document.querySelector('#resultGroup table') !== null"
                " || document.querySelector('#resultGroup .ui-datatable') !== null"
                " || (document.querySelector('#resultGroup') && document.querySelector('#resultGroup').innerText.length > 40)",
                timeout=30000,
            )

            # Pull the current page's rows. Results are paginated ~10 per
            # page via a PrimeFaces/Mojarra paginator (#tableForm:pagination).
            # We click "Seguinte" until it no longer advances to collect
            # the full list.
            extract_js = """
                () => {
                    const out = [];
                    const root = document.querySelector('#resultGroup');
                    if (!root) return out;
                    const tbl = root.querySelector('table');
                    if (!tbl) return out;
                    for (const tr of tbl.querySelectorAll('tbody tr')) {
                        const cells = Array.from(tr.querySelectorAll('td'))
                            .map(td => td.innerText.trim());
                        if (cells.length < 3) continue;
                        if (!/^\\d{9}$/.test(cells[1])) continue;
                        out.push({ nome: cells[0], nipc: cells[1], alvara_codes: cells[2] });
                    }
                    return out;
                }
            """

            aggregated: dict[str, dict] = {}
            seen_signatures: set[str] = set()
            page_num = 0
            while page_num < 50:  # hard cap for safety
                page_num += 1
                batch = await page.evaluate(extract_js)
                sig = "|".join(sorted(r["nipc"] for r in batch))
                if not batch or sig in seen_signatures:
                    break
                seen_signatures.add(sig)
                for r in batch:
                    if r["nipc"] not in aggregated:
                        aggregated[r["nipc"]] = r
                logger.info(
                    "PSP: page %d — %d rows (running total: %d companies)",
                    page_num, len(batch), len(aggregated),
                )
                # Find the "Seguinte" link (id ending with j_idt98 or text).
                advanced = await page.evaluate(
                    """
                    () => {
                        const pagination = document.querySelector('#tableForm\\\\:pagination');
                        if (!pagination) return false;
                        const links = Array.from(pagination.querySelectorAll('a'));
                        const next = links.find(a =>
                            /Seguinte/i.test(a.innerText || '') && !a.classList.contains('ui-state-disabled')
                        );
                        if (!next) return false;
                        next.click();
                        return true;
                    }
                    """
                )
                if not advanced:
                    break
                # Wait for the table contents to settle (any change to
                # row signatures). Retry with a short sleep.
                for _ in range(30):
                    await page.wait_for_timeout(300)
                    cur = await page.evaluate(
                        """
                        () => {
                            const tbl = document.querySelector('#resultGroup table');
                            if (!tbl) return '';
                            return Array.from(tbl.querySelectorAll('tbody tr'))
                                .filter(tr => tr.querySelectorAll('td').length >= 3)
                                .map(tr => tr.querySelectorAll('td')[1].innerText.trim())
                                .sort().join('|');
                        }
                        """
                    )
                    if cur and cur != sig:
                        break

            logger.info(
                "PSP: collected %d distinct companies across %d page(s)",
                len(aggregated), page_num,
            )
            return [
                {
                    "nipc": r["nipc"],
                    "nome": _clean_name(r["nome"]),
                    "alvara_number": _normalise_codes(r["alvara_codes"]),
                    "morada": None, "localidade": None,
                    "telefone": None, "email": None,
                }
                for r in aggregated.values() if r["nipc"]
            ]
        finally:
            await ctx.close()
            await browser.close()


def _clean_name(s: str | None) -> str | None:
    if not s:
        return None
    # Some cells come with newline-joined duplicates from rowspan expansion
    # (e.g. "LEI-34/2013\nLEI-34/2013"). Take the first distinct non-empty
    # line and trim.
    parts = [p.strip() for p in s.split("\n") if p.strip()]
    if not parts:
        return None
    return parts[0][:200]


def _normalise_codes(raw: str | None) -> str | None:
    """Accept "16A\n16B\n16C" and emit "16A, 16B, 16C" deduped in input order."""
    if not raw:
        return None
    seen: list[str] = []
    for tok in raw.split("\n"):
        tok = tok.strip()
        if tok and tok not in seen:
            seen.append(tok)
    return ", ".join(seen)[:100] if seen else None
