"""Revise each located table's box to the true end of its content.

The Set-of-Mark locator and Camelot both bound a table generously and can run
their box past the last data row, enclosing footnote lines printed beneath the
grid. The corrected markdown, by contrast, ends at the last tabular row. Matching
that last row's tokens back to the page text layer locates the real bottom, so
`content_region` is `som_region` with its bottom moved to it.

`apply_content_regions` sets `content_region` on every Set-of-Mark table, reading
each page's text layer once. The new bottom is the lowest matched word's bottom
edge, with no clamp to the original. It normally sits above `som_region`'s bottom,
but can sit slightly below it when a matched word straddles that edge. When the
markdown is empty (no grid) or the row cannot be located it leaves
`content_region` equal to `som_region`, never inventing a tighter box.
"""

from __future__ import annotations

import re
from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Sequence, Tuple

from quber.agents.completeness import page_words
from quber.core.extractors.base import ExtractedTable

Region = Tuple[float, float, float, float]
Word = Tuple[float, float, float, float, str]


def normalize(token: str) -> str:
    """A token reduced to lowercase alphanumerics, for text-layer matching."""
    return re.sub(r"[^a-z0-9]", "", token.lower())


def last_row_tokens(markdown: str) -> set[str]:
    """Distinctive tokens of the markdown's last data row (two chars or more).

    The last row carries the table's bottom edge; its label plus values are
    matched against the page text to find where that row sits.
    """
    rows = [
        line
        for line in markdown.splitlines()
        if line.strip().startswith("|") and not set(line.strip()) <= set("|-: ")
    ]
    if not rows:
        return set()
    toks = {normalize(w) for cell in rows[-1].split("|") for w in cell.split()}
    return {t for t in toks if len(t) >= 2}


def revise_content_region(
    som_region: Region, markdown: str, words: Sequence[Word], page_w: float, page_h: float
) -> Region:
    """`som_region` with the bottom moved to the table's last data row.

    The last row's tokens are matched to page words inside som_region's own x/y
    band, so a stacked sibling's identical last row is never picked up, and the
    lowest bottom of the matched words becomes the new bottom edge, unclamped.
    A word counts as inside when its center is within 3 points of the band, so
    the new bottom can fall just below som_region's. Returns `som_region`
    unchanged when the markdown has no rows or the row cannot be located.
    """
    tokens = last_row_tokens(markdown)
    if not tokens:
        return som_region
    x0, y0, x1, y1 = som_region
    lo_x, hi_x = min(x0, x1) * page_w, max(x0, x1) * page_w
    lo_y, hi_y = min(y0, y1) * page_h, max(y0, y1) * page_h
    bottoms = [
        w[3]
        for w in words
        if lo_x - 3 <= (w[0] + w[2]) / 2 <= hi_x + 3
        and lo_y - 3 <= (w[1] + w[3]) / 2 <= hi_y + 3
        and normalize(w[4]) in tokens
    ]
    if not bottoms:
        return som_region
    return (x0, y0, x1, max(bottoms) / page_h)


def apply_content_regions(tables: List[ExtractedTable], source: str) -> None:
    """Set `content_region` on every located table. Mutates in place.

    Reads each page's text layer once. A table with no `som_region` (no locator)
    is left untouched; otherwise `content_region` is the located region with its
    bottom revised to the last tabular row, or the located region itself when no
    revision can be made.
    """
    by_page: Dict[int, List[ExtractedTable]] = defaultdict(list)
    for table in tables:
        if table.som_region is not None:
            by_page[table.page].append(table)

    for page, page_tables in by_page.items():
        page_w, page_h, words = page_words(Path(source), page)
        for table in page_tables:
            assert table.som_region is not None  # filtered above
            table.content_region = revise_content_region(
                table.som_region, table.markdown, words, page_w, page_h
            )
