"""
ANAF lista albă (contribuabili FĂRĂ obligații restante) — live scraper.

Source: https://www.anaf.ro/restante/listaalba.xhtml (JSF/PrimeFaces, JCaptcha image).
SAME mechanism as anaf_datornici scraper, but different endpoint and 3-column row layout.

Mechanism (per probe 2026-05-12):
  1. GET /restante/listaalba.xhtml → extract `javax.faces.ViewState` + session cookies
  2. GET /restante/kaptcha.jpg (same session)
  3. POST kaptcha image to 2captcha (~$0.0005) → get 5-char text token
  4. POST /restante/listaalba.xhtml with captcha + form fields → first page of data
  5. AJAX PrimeFaces pagination POSTs for subsequent pages (no new captcha)
  6. Parse <tr data-ri=N> rows, extract 3 cells per row (nr_crt, name, cui),
     UPSERT to anaf.lista_alba

Site shows CURRENT QUARTER ONLY (no historical access). Each quarterly run
captures one snapshot.

Env vars:
  TWOCAPTCHA_KEY     — required (image solver)
  DATABASE_URL       — postgres conn string (Prisma-style ?schema= stripped)
  DRY_RUN=1          — parse plan, no captcha, no DB writes
  ROWS_PER_PAGE=1000 — pagination chunk size
  ANAF_LISTA_ALBA_LOG — log path (default stderr)
"""

from __future__ import annotations

import base64
import logging
import os
import re
import sys
import time
from dataclasses import dataclass, field
from datetime import date
from typing import Any

import psycopg2
import psycopg2.extras
import requests

# ─────────────────────────────────────────────────────────────────────────────
# Logging

LOG_FILE = os.environ.get("ANAF_LISTA_ALBA_LOG", "")
_handlers: list[logging.Handler] = [logging.StreamHandler(sys.stderr)]
if LOG_FILE:
    try:
        _handlers.append(logging.FileHandler(LOG_FILE))
    except OSError:
        pass
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=_handlers,
)
log = logging.getLogger("anaf_lista_alba")


# ─────────────────────────────────────────────────────────────────────────────
# Constants

BASE = "https://www.anaf.ro/restante"
INDEX_PAGE = f"{BASE}/listaalba.xhtml"
INDEX_FORM = INDEX_PAGE  # form action POSTs to the same URL
KAPTCHA_URL = f"{BASE}/kaptcha.jpg"
USER_AGENT = (
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/120.0 Safari/537.36"
)
TIMEOUT = 60

TWOCAPTCHA_IN = "https://2captcha.com/in.php"
TWOCAPTCHA_RES = "https://2captcha.com/res.php"
TWOCAPTCHA_POLL_INTERVAL = 5
TWOCAPTCHA_MAX_POLL = 36       # 36 * 5s = 180s
TWOCAPTCHA_MAX_ATTEMPTS = 3
TWOCAPTCHA_REPORT = True

DEFAULT_ROWS_PER_PAGE = int(os.environ.get("ROWS_PER_PAGE", "1000"))


# ─────────────────────────────────────────────────────────────────────────────
# Quarter math

def parse_publication_date(html: str) -> tuple[date, str]:
    """Extract 'la data de DD.MM.YYYY' from page."""
    m = re.search(r"data\s+de\s+(\d{2})\.(\d{2})\.(\d{4})", html, re.IGNORECASE)
    if not m:
        raise RuntimeError("Cannot parse publication_date from page HTML")
    d, mo, y = int(m.group(1)), int(m.group(2)), int(m.group(3))
    pub_date = date(y, mo, d)
    q = (mo - 1) // 3 + 1
    period_label = f"T{q} {y}"
    return pub_date, period_label


# ─────────────────────────────────────────────────────────────────────────────
# 2captcha image solver

def solve_kaptcha(api_key: str, image: bytes, *, attempt: int = 1) -> tuple[str, str]:
    b64 = base64.b64encode(image).decode()
    r = requests.post(
        TWOCAPTCHA_IN,
        data={
            "key": api_key,
            "method": "base64",
            "body": b64,
            "json": "1",
            "numeric": "0",
            "min_len": "4",
            "max_len": "8",
            "language": "2",
            "regsense": "1",
        },
        timeout=TIMEOUT,
    )
    r.raise_for_status()
    j = r.json()
    if j.get("status") != 1:
        raise RuntimeError(f"2captcha in.php error: {j}")
    cid = j["request"]
    log.info(f"2captcha attempt {attempt}: id={cid}, polling…")

    for _ in range(TWOCAPTCHA_MAX_POLL):
        time.sleep(TWOCAPTCHA_POLL_INTERVAL)
        rr = requests.get(
            TWOCAPTCHA_RES,
            params={"key": api_key, "action": "get", "id": cid, "json": "1"},
            timeout=TIMEOUT,
        )
        jj = rr.json()
        if jj.get("status") == 1:
            return jj["request"], cid
        if jj.get("request") in ("CAPCHA_NOT_READY", "CAPTCHA_NOT_READY"):
            continue
        raise RuntimeError(f"2captcha res.php error: {jj}")
    raise RuntimeError(f"2captcha timeout after {TWOCAPTCHA_MAX_POLL*TWOCAPTCHA_POLL_INTERVAL}s")


def report_bad_solve(api_key: str, cid: str) -> None:
    try:
        requests.get(
            TWOCAPTCHA_RES,
            params={"key": api_key, "action": "reportbad", "id": cid},
            timeout=TIMEOUT,
        )
    except Exception:
        pass


# ─────────────────────────────────────────────────────────────────────────────
# Session / pagination

@dataclass
class AnafSession:
    api_key: str
    s: requests.Session = field(default_factory=requests.Session)
    viewstate: str = ""
    publication_date: date | None = None
    period_label: str = ""

    def __post_init__(self):
        self.s.headers.update({"User-Agent": USER_AGENT})

    def bootstrap(self) -> None:
        log.info(f"GET {INDEX_PAGE}")
        r = self.s.get(INDEX_PAGE, timeout=TIMEOUT)
        r.raise_for_status()
        m = re.search(r'name="javax\.faces\.ViewState"[^>]*value="([^"]+)"', r.text)
        if not m:
            raise RuntimeError("No ViewState in initial page")
        self.viewstate = m.group(1)
        log.info(f"viewstate fetched ({len(self.viewstate)} chars)")

    def get_kaptcha(self) -> bytes:
        r = self.s.get(
            KAPTCHA_URL, timeout=TIMEOUT, headers={"Referer": INDEX_PAGE}
        )
        r.raise_for_status()
        if not r.content.startswith(b"\xff\xd8\xff"):
            raise RuntimeError("kaptcha response not JPEG")
        return r.content

    def submit_initial(self, captcha_text: str, rows_per_page: int) -> str:
        log.info(f"POST {INDEX_FORM} (captcha={captcha_text!r}, rows={rows_per_page})")
        r = self.s.post(
            INDEX_FORM,
            data={
                "form": "form",
                "form:inputc": captcha_text,
                "form:searchdata": "",
                "form:submit": "",
                "form_SUBMIT": "1",
                "javax.faces.ViewState": self.viewstate,
            },
            headers={
                "Referer": INDEX_PAGE,
                "Origin": "https://www.anaf.ro",
                "User-Agent": USER_AGENT,
                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Accept-Language": "ro,en;q=0.9",
                "Content-Type": "application/x-www-form-urlencoded",
            },
            timeout=TIMEOUT,
        )
        r.raise_for_status()
        m = re.search(r'name="javax\.faces\.ViewState"[^>]*value="([^"]+)"', r.text)
        if m:
            self.viewstate = m.group(1)
        if "Cod de validare gresit" in r.text or "incorect" in r.text.lower()[:5000]:
            raise CaptchaWrong(r.text)
        try:
            self.publication_date, self.period_label = parse_publication_date(r.text)
            log.info(f"publication_date={self.publication_date} period={self.period_label}")
        except RuntimeError:
            log.warning("could not parse publication_date — using today's quarter")
            today = date.today()
            self.publication_date = today
            self.period_label = f"T{(today.month - 1) // 3 + 1} {today.year}"
        return r.text

    def fetch_page(self, first: int, rows_per_page: int) -> str:
        r = self.s.post(
            INDEX_FORM,
            data={
                "javax.faces.partial.ajax": "true",
                "javax.faces.source": "form:dataTable",
                "javax.faces.partial.execute": "form:dataTable",
                "javax.faces.partial.render": "form:dataTable",
                "form:dataTable": "form:dataTable",
                "form:dataTable_pagination": "true",
                "form:dataTable_first": str(first),
                "form:dataTable_rows": str(rows_per_page),
                "form:dataTable_encodeFeature": "true",
                "form": "form",
                "form:inputc": "",
                "form:searchdata": "",
                "javax.faces.ViewState": self.viewstate,
            },
            headers={
                "Referer": INDEX_FORM,
                "Origin": "https://www.anaf.ro",
                "User-Agent": USER_AGENT,
                "Accept": "application/xml,text/xml,*/*;q=0.01",
                "Accept-Language": "ro,en;q=0.9",
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
                "X-Requested-With": "XMLHttpRequest",
                "Faces-Request": "partial/ajax",
            },
            timeout=TIMEOUT,
        )
        r.raise_for_status()
        m = re.search(r'<update id="[^"]*javax\.faces\.ViewState[^"]*"><!\[CDATA\[([^\]]+)\]\]>', r.text)
        if m:
            self.viewstate = m.group(1)
        return r.text


class CaptchaWrong(Exception):
    pass


# ─────────────────────────────────────────────────────────────────────────────
# Row parsing — only 3 columns in lista_alba

def parse_rows(html_or_partial: str) -> list[dict[str, Any]]:
    """Extract rows from initial HTML or AJAX partial.

    Row layout (3 cells per probe 2026-05-12):
       0: nr_crt
       1: name (denumirea contribuabilului)
       2: CIF (cui)
    """
    rows: list[dict[str, Any]] = []
    for tr_m in re.finditer(
        r'<tr\b[^>]*data-ri="(\d+)"[^>]*>(.*?)</tr>',
        html_or_partial, re.DOTALL,
    ):
        body = tr_m.group(2)
        cells = re.findall(r"<td\b[^>]*>(.*?)</td>", body, re.DOTALL)
        if len(cells) < 3:
            continue
        def _txt(s: str) -> str:
            t = re.sub(r"<[^>]+>", "", s)
            return re.sub(r"\s+", " ", t).strip()
        rows.append({
            "nr_crt": _txt(cells[0]),
            "name": _txt(cells[1]),
            "cui": _txt(cells[2]),
        })
    return rows


# ─────────────────────────────────────────────────────────────────────────────
# DB UPSERT

def upsert_rows(
    conn,
    rows: list[dict[str, Any]],
    publication_date: date,
    period_label: str,
) -> int:
    if not rows:
        return 0
    source_url = INDEX_PAGE
    payload = [(
        r["cui"].replace(" ", "").upper().lstrip("RO"),
        r["name"],
        publication_date,
        period_label,
        source_url,
    ) for r in rows if r["cui"]]

    sql = """
        INSERT INTO anaf.lista_alba (
            cui, name, publication_date, period_label, source_url
        ) VALUES %s
        ON CONFLICT (cui, publication_date)
        DO UPDATE SET
            name = EXCLUDED.name,
            period_label = EXCLUDED.period_label,
            source_url = EXCLUDED.source_url,
            fetched_at = now()
    """
    with conn.cursor() as cur:
        psycopg2.extras.execute_values(cur, sql, payload, page_size=1000)
    conn.commit()
    return len(payload)


# ─────────────────────────────────────────────────────────────────────────────
# Orchestration

def run(*, dry_run: bool, rows_per_page: int) -> dict[str, int]:
    api_key = os.environ.get("TWOCAPTCHA_KEY", "")
    if not api_key and not dry_run:
        raise RuntimeError("Missing TWOCAPTCHA_KEY env var")

    if dry_run:
        log.info("DRY_RUN=1 — connecting only to validate config, no captcha solve")
        sess = AnafSession(api_key="")
        sess.bootstrap()
        log.info(f"bootstrap OK, viewstate captured ({len(sess.viewstate)} chars)")
        log.info(f"would solve 1 captcha (~$0.001 worst case) then paginate {rows_per_page} rows/page")
        return {"lista_alba_inserted": 0, "errors": 0}

    db_url = os.environ.get("DATABASE_URL", "")
    if not db_url:
        raise RuntimeError("Missing DATABASE_URL env var")
    db_url = re.sub(r"[?&]schema=[^&]*", "", db_url)
    db_url = re.sub(r"\?$", "", db_url)
    conn = psycopg2.connect(db_url)
    conn.autocommit = False

    sess = AnafSession(api_key=api_key)

    last_cid: str | None = None
    initial_html = ""
    for attempt in range(1, TWOCAPTCHA_MAX_ATTEMPTS + 1):
        sess.bootstrap()
        image = sess.get_kaptcha()
        token, cid = solve_kaptcha(api_key, image, attempt=attempt)
        last_cid = cid
        try:
            initial_html = sess.submit_initial(token, rows_per_page)
            log.info(f"captcha accepted on attempt {attempt}")
            break
        except CaptchaWrong:
            log.warning(f"captcha rejected by ANAF on attempt {attempt}, retrying")
            if TWOCAPTCHA_REPORT and last_cid:
                report_bad_solve(api_key, last_cid)
            if attempt == TWOCAPTCHA_MAX_ATTEMPTS:
                raise RuntimeError("captcha solve failed after retries")

    all_rows = parse_rows(initial_html)
    log.info(f"page 1: {len(all_rows)} rows")

    first = len(all_rows)
    page_num = 2
    while True:
        try:
            partial = sess.fetch_page(first=first, rows_per_page=rows_per_page)
            new_rows = parse_rows(partial)
            if not new_rows:
                log.info(f"pagination exhausted at first={first}")
                break
            all_rows.extend(new_rows)
            log.info(f"page {page_num}: {len(new_rows)} rows (running total: {len(all_rows)})")
            first += len(new_rows)
            page_num += 1
        except Exception as e:
            log.error(f"pagination error at page {page_num}: {e}")
            break

    log.info(f"total rows collected: {len(all_rows)}")
    if not sess.publication_date:
        raise RuntimeError("No publication_date captured")

    inserted = upsert_rows(
        conn, all_rows,
        publication_date=sess.publication_date,
        period_label=sess.period_label,
    )
    log.info(f"upserted {inserted} rows into anaf.lista_alba for {sess.period_label}")

    conn.close()
    return {"lista_alba_inserted": inserted, "errors": 0}


def main():
    dry_run = os.environ.get("DRY_RUN", "0") == "1"
    rows_per_page = int(os.environ.get("ROWS_PER_PAGE", str(DEFAULT_ROWS_PER_PAGE)))
    log.info(f"=== ANAF lista_alba scrape: dry_run={dry_run} rows_per_page={rows_per_page} ===")
    try:
        result = run(dry_run=dry_run, rows_per_page=rows_per_page)
    except Exception as e:
        log.error(f"FATAL: {e}", exc_info=True)
        sys.exit(1)
    log.info(f"DONE {result}")


if __name__ == "__main__":
    main()
