"""Phase 3 — table extraction via PaddleOCR PP-Structure.

The personnel section of a surat sprint is almost always a table with columns
like (No, Pangkat, NRP, Nama, Jabatan dalam Dinas, Jabatan dalam Sprint,
Keterangan). Plain OCR on the page produces a flat stream of text lines that
makes column reconstruction brittle, so we use PP-Structure's table recognizer
which returns a 2D cell grid directly.

Like the OCR engine wrapper, PP-Structure has a heavy initialization cost
(~3-6s on CPU) and an API that has shifted across paddleocr releases, so we
hide it behind a small process-global accessor and a stable dataclass surface.

Tests do NOT require paddleocr installed — `extract_tables_from_html` and the
personnel column mapper are pure-Python and parse PP-Structure's HTML output.
"""

from __future__ import annotations

import html
import re
from dataclasses import dataclass, field
from threading import Lock
from typing import TYPE_CHECKING

from ocr_sprint.config import get_settings
from ocr_sprint.pipeline.ingest import NDArrayU8
from ocr_sprint.utils.logging import get_logger

if TYPE_CHECKING:
    from paddleocr import PPStructure

_logger = get_logger(__name__)
_lock = Lock()
_instance: PPStructure | None = None


@dataclass(frozen=True)
class TableCell:
    """One parsed table cell."""

    text: str
    row: int
    col: int


@dataclass
class DetectedTable:
    """One table region detected by PP-Structure, parsed into a 2D grid.

    `cells[r]` is a list of strings for row r. The list is ragged if the table
    has merged cells (we don't currently un-merge), so callers should treat it
    defensively.
    """

    cells: list[list[str]] = field(default_factory=list)
    html: str = ""

    @property
    def n_rows(self) -> int:
        return len(self.cells)

    @property
    def n_cols(self) -> int:
        return max((len(r) for r in self.cells), default=0)


# ---------- PP-Structure singleton ----------


def _create_pp_structure(
    pp_structure_cls: type[PPStructure], pp_lang: str, use_gpu: bool
) -> PPStructure:
    # layout=True so that PP-Structure also returns figure/text regions; we
    # filter to tables only afterwards. show_log=False to keep stdout clean.
    return pp_structure_cls(
        lang=pp_lang,
        use_gpu=use_gpu,
        layout=True,
        show_log=False,
    )


def _build_pp_structure() -> PPStructure:
    s = get_settings()
    if s.ocr_use_gpu:
        from ocr_sprint.utils.gpu import configure_nvidia_dll_path

        configure_nvidia_dll_path()

    from paddleocr import PPStructure

    # PPStructure layout models only support 'en' and 'ch', not 'latin'.
    # Use 'en' for layout/table detection — it's language-agnostic (detects
    # table structure, not text language). OCR within cells still works for
    # Indonesian text because the recognition model handles Latin scripts.
    pp_lang = "en" if s.ocr_lang not in ("en", "ch") else s.ocr_lang
    _logger.info("pp_structure.init", lang=pp_lang, use_gpu=s.ocr_use_gpu)
    try:
        return _create_pp_structure(PPStructure, pp_lang, s.ocr_use_gpu)
    except Exception as exc:
        if not s.ocr_use_gpu:
            raise
        _logger.warning("pp_structure.gpu_init_failed_falling_back_cpu", error=str(exc))
        return _create_pp_structure(PPStructure, pp_lang, False)


def get_pp_structure() -> PPStructure:
    """Lazy, thread-safe singleton accessor for PP-Structure."""
    global _instance
    if _instance is None:
        with _lock:
            if _instance is None:
                _instance = _build_pp_structure()
    return _instance


def warmup() -> None:
    """Eagerly initialize the PP-Structure engine.

    Call this during application startup so the first real request does not
    pay the model-loading cost (~3-6s on CPU). Mirrors ocr.warmup() so the
    lifespan handler can warm both engines in one place.
    """
    _logger.info("pp_structure.warmup.start")
    get_pp_structure()
    _logger.info("pp_structure.warmup.done")


# ---------- table parsing ----------


_TR_RE = re.compile(r"<tr[^>]*>(.*?)</tr>", re.IGNORECASE | re.DOTALL)
_TD_RE = re.compile(r"<t[dh][^>]*>(.*?)</t[dh]>", re.IGNORECASE | re.DOTALL)
_TAG_RE = re.compile(r"<[^>]+>")


def _strip_html(fragment: str) -> str:
    """Remove inner tags + collapse whitespace + decode HTML entities."""
    no_tags = _TAG_RE.sub(" ", fragment)
    decoded = html.unescape(no_tags)
    return " ".join(decoded.split()).strip()


def parse_table_html(table_html: str) -> list[list[str]]:
    """Parse an HTML <table> string into a 2D list of cell text values.

    Tolerant to PP-Structure's slight HTML inconsistencies (no closing tags,
    nested spans, &nbsp; entities) — we don't need full HTML compliance,
    just rows x cells.
    """
    rows: list[list[str]] = []
    for tr in _TR_RE.findall(table_html):
        cells = [_strip_html(td) for td in _TD_RE.findall(tr)]
        rows.append(cells)
    return rows


def extract_tables_from_pp_result(
    pp_result: list[dict[str, object]],
) -> list[DetectedTable]:
    """Pull tables out of PP-Structure's region list.

    PP-Structure returns one dict per detected region; tables have
    `type == "table"` and the recognized table HTML inside `res["html"]`.
    """
    tables: list[DetectedTable] = []
    for region in pp_result:
        if region.get("type") != "table":
            continue
        res = region.get("res")
        if not isinstance(res, dict):
            continue
        table_html = res.get("html", "")
        if not isinstance(table_html, str) or not table_html:
            continue
        cells = parse_table_html(table_html)
        if not cells:
            continue
        tables.append(DetectedTable(cells=cells, html=table_html))
    return tables


def run_table_extraction(image: NDArrayU8) -> list[DetectedTable]:
    """Run PP-Structure on a single page and return the parsed tables."""
    engine = get_pp_structure()
    raw = engine(image)
    if not isinstance(raw, list):
        return []
    return extract_tables_from_pp_result(raw)
