"""The track's graft: supplant content readings, preserve role-labelled text.

The scan's reading replaces docling's stray text fragments under a chart — two
readings of one chart must not coexist. But docling labels its text items, and
an item labelled footnote or caption is not a reading of the chart: it is
native page text with a known role. The shared
`quber.core.figures.graft.graft_figures` deletes every text child of a read
picture except the labels in its `preserve` argument. The dpt-2 path passes
none, so a footnote the parse filed under a read picture is deleted and the
correction agent's read of the page image is its only record. This wrapper
passes `PRESERVED_LABELS`, which is what gives footnote resolution its second
source.

Everything else — how figures and pictures are matched, how a region is
reshaped to hold one picture per figure, what lands on the picture — is that
shared machinery, unchanged.
"""

from __future__ import annotations

from pathlib import Path
from typing import Dict, List, Optional, Tuple

from docling_core.types.doc.base import BoundingBox, CoordOrigin
from docling_core.types.doc.common.reference import ProvenanceItem
from docling_core.types.doc.document import DoclingDocument
from docling_core.types.doc.labels import DocItemLabel
from loguru import logger

from quber.core.extractors.base import ExtractedTable
from quber.core.figures.geometry import NormBox, norm_box, prov_box
from quber.core.figures.graft import graft_figures as shared_graft_figures
from quber.core.figures.models import PageScan
from quber.core.fusion.graft import attach_attribution, markdown_to_table_data, reading_order_anchor

#: The labels that survive under a picture the scan has read. A footnote or a
#: caption is page text with a role, not a reading of the figure.
PRESERVED_LABELS = (DocItemLabel.FOOTNOTE, DocItemLabel.CAPTION)


def graft_figures(
    document: DoclingDocument,
    scans: List[PageScan],
    page_dims: Optional[Dict[int, Tuple[float, float]]] = None,
    source: Optional[Path] = None,
) -> Tuple[DoclingDocument, List[str]]:
    """Carry each scan's figure text onto the matching picture, keeping
    footnote- and caption-labelled items in the document."""
    return shared_graft_figures(document, scans, page_dims, source=source, preserve=PRESERVED_LABELS)


def insert_orphan_tables(
    document: DoclingDocument,
    scans: List[PageScan],
    tables: List[ExtractedTable],
    page_dims: Dict[int, Tuple[float, float]],
) -> List[str]:
    """Insert each captured table the document held no element for.

    The capture step vetted the grid the way it vets every scanned table, and
    the region it stands on holds no table, no picture and no parse text — a
    map's legend is the standing example — so the document gains a table at
    the region's reading-order position, attributed like any other. Without
    this the page's only reading of those values stays outside the document.

    Mutates `document`. Each inserted grid's record is given the new table's
    reference. Returns one error per orphan whose extraction produced no body.
    """
    by_id = {t.table_id: t for t in tables if t.table_id}
    errors: List[str] = []
    for scan in scans:
        for scanned in scan.tables:
            if scanned.table_id is None or scanned.table_ref is not None or scanned.picture_ref is not None:
                continue
            extracted = by_id.get(scanned.table_id)
            if extracted is None or not (extracted.markdown or "").strip():
                errors.append(
                    f"page {scan.page}: the scan read a table ({scanned.chunk_id}) over a region "
                    "the parse holds nothing for, and its extraction produced no body; the values "
                    "are in the table records only"
                )
                continue
            data = markdown_to_table_data(extracted.markdown)
            prov = _table_prov(scanned.box, scan.page, page_dims)
            sibling, after = reading_order_anchor(document, extracted, page_dims)
            if sibling is None:
                inserted = document.add_table(data=data, prov=prov)
            else:
                inserted = document.insert_table(sibling=sibling, data=data, prov=prov, after=after)
            attach_attribution(document, inserted, extracted, page_dims)
            scanned.table_ref = inserted.self_ref
            logger.info(
                "Orphan table: page {} inserted the scan's reading ({}) as {}",
                scan.page,
                scanned.table_id,
                inserted.self_ref,
            )
    return errors


def _table_prov(
    box: Optional[Dict[str, float]],
    page: int,
    page_dims: Dict[int, Tuple[float, float]],
) -> Optional[ProvenanceItem]:
    """A provenance record for an inserted table, its box in bottom-left points."""
    normalized = norm_box(box)
    if normalized is None:
        return None
    width, height = page_dims.get(page, (612.0, 792.0))
    x1, y1, x2, y2 = normalized
    bbox = BoundingBox(
        l=x1 * width,
        r=x2 * width,
        t=(1.0 - y1) * height,
        b=(1.0 - y2) * height,
        coord_origin=CoordOrigin.BOTTOMLEFT,
    )
    return ProvenanceItem(page_no=page, bbox=bbox, charspan=(0, 0))


def unhomed_tables(
    scans: List[PageScan],
    document: DoclingDocument,
    page_dims: Dict[int, Tuple[float, float]],
) -> List[str]:
    """One flag per scanned table over a region the document holds nothing for.

    The scan reads a stat-panel collage as a table, and the parse routinely
    holds neither a table nor a picture there — the region is native text. The
    capture step then never extracts it, and without this check the table would
    vanish with no record that anything had been read. The cells stay in the
    digest records, and the page's own text is grouped by the same node's
    rectangles, so the flag is a pointer for review rather than a loss report.

    A region any table in the document shares any area with on its page is not
    flagged, however small the overlap: the table engine is authoritative for
    text-layer tables, and the scan re-reading one is redundancy by design, not
    a loss.
    """
    tables_by_page: Dict[int, List[NormBox]] = {}
    for item in document.tables:
        if not item.prov:
            continue
        page = item.prov[0].page_no
        width, height = page_dims.get(page, (612.0, 792.0))
        box = prov_box(item, width, height)
        if box is not None:
            tables_by_page.setdefault(page, []).append(box)

    flags: List[str] = []
    for scan in scans:
        for table in scan.tables:
            if table.table_id is not None or table.table_ref is not None or table.picture_ref is not None:
                continue
            box = norm_box(table.box)
            if box is not None and any(_overlaps(box, other) for other in tables_by_page.get(scan.page, [])):
                continue
            flags.append(
                f"page {scan.page}: the scan returned a table ({table.chunk_id}) over a region "
                "the parse holds no table or picture for; its cells are in the digest records "
                "and the page's own text is grouped by its rectangles"
            )
    return flags


def _overlaps(one: NormBox, other: NormBox) -> bool:
    """Do the two boxes share any area at all?"""
    return min(one[2], other[2]) > max(one[0], other[0]) and min(one[3], other[3]) > max(one[1], other[1])
