"""Turn a cell grid and its geometry into a finished `ExtractedTable`.

Everything after the cells are in hand is the same work whichever tool produced
them: correct the structure against the table's image, ground every corrected
cell in a measured box, verify the statuses that carry no box, and record what
the header area printed that no cell carries.

Two callers hand cells in. The Set-of-Mark pipeline hands in a region-constrained
Camelot grid. The page-scan workflow hands in the grid a scan read off a table
printed as an image, where there was no text layer for Camelot to read. Both get
back the same object, filled the same way, because the correction and grounding
steps read the table's image and the page's own text and never ask which tool
found the cells.
"""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from pathlib import Path
from typing import List, Literal, Optional, Tuple

from quber.agents.llm_client import LLMClient
from quber.agents.status_inspector import StatusInspector
from quber.core.extractors.base import (
    ExtractedTable,
    MergedCellBox,
    grid_fingerprint,
    grounded_grid,
    table_address,
)
from quber.core.extractors.camelot.acquire import grid_to_markdown
from quber.core.extractors.camelot.correspondence.correction import correct_structure, printed_title
from quber.core.extractors.set_of_mark.inspection import inspect_gap_cells
from quber.core.extractors.set_of_mark.merge_grounding import (
    find_dropped_header_text,
    locate_markers,
    log_ungrounded,
    log_ungrounded_cells,
    resolve_corrected_grid,
    resolve_merges,
)

Box = Tuple[float, float, float, float]
NormBox = Tuple[float, float, float, float]
TableKind = Literal["text_table", "chart", "image_table"]


@dataclass
class TableAssembly:
    """Run-scoped inputs the assembly needs, the same for every table in a run."""

    source: Path
    llm: LLMClient
    correct_sem: asyncio.Semaphore
    dpi: int
    # Verifies each unboxed cell's proposed status against the table image; None
    # disables inspection and the proposed statuses stand.
    inspector: Optional[StatusInspector] = None


@dataclass
class CamelotOrigin:
    """The Camelot extraction a table's cells came from.

    Absent for cells no Camelot pass produced, whose `bbox`, `flavor` and
    `camelot_accuracy` then keep their defaults, because there is no extraction
    for those fields to describe.
    """

    bbox: Optional[Box]
    flavor: Literal["lattice", "stream", "unknown"]
    accuracy: float


async def assemble_table(
    assembly: TableAssembly,
    page: int,
    ordinal: int,
    page_image: Path,
    page_dims: Tuple[float, float],
    cells: List[List[str]],
    cell_boxes: List[List[Optional[Box]]],
    bbox: Optional[Box],
    title: str = "",
    som_region: Optional[NormBox] = None,
    kind: TableKind = "text_table",
    camelot: Optional[CamelotOrigin] = None,
    ground_values: bool = True,
) -> ExtractedTable:
    """One table, corrected and grounded, from its cells and their boxes.

    `bbox` is the table's box in PDF points with a bottom-left page origin. It
    scopes the image crop the correction reads and the region text it is given.
    The grounding guard, when `ground_values` is on, accepts a number printed
    anywhere on the page, not only inside `bbox`. Without `bbox` there is
    nothing to crop, so the cells are rendered as they arrived and the fields the
    correction fills stay empty.

    `cell_boxes` is shaped exactly like `cells`, in the same frame as `bbox`, and
    may hold None wherever no box was measured. `title` is the identity read off
    the page image before the cells were captured; it stands only when no
    correction ran, and only if the page prints it. A correction's title is the
    printed name copied off the page, or empty when none is printed, and that is
    what the table carries.
    """
    page_w, page_h = page_dims
    correction = await correct_structure(
        cells,
        page_image,
        bbox,
        str(assembly.source),
        page,
        assembly.llm,
        assembly.correct_sem,
        assembly.dpi,
        ground_values,
    )
    # The pre-correction grid paired with per-cell geometry. The correction can
    # change or add values (a repaired misread on an image table, a row
    # recovered from the text layer), so a corrected cell can differ in value
    # from every cell here. Boxes normalize to the page frame here so the whole
    # table reads in one frame.
    cell_grid = grounded_grid(cells, cell_boxes, page_w, page_h)

    table_id = table_address(assembly.source, page, ordinal)
    fingerprint = grid_fingerprint(cells)
    origin_bbox = camelot.bbox if camelot else None
    flavor: Literal["lattice", "stream", "unknown"] = camelot.flavor if camelot else "unknown"
    accuracy = camelot.accuracy if camelot else 0.0

    if correction is None:
        return ExtractedTable(
            table_id=table_id,
            content_fingerprint=fingerprint,
            title=await printed_title(title, str(assembly.source), page),
            markdown=grid_to_markdown(cells),
            page=page,
            source=str(assembly.source),
            som_region=som_region,
            kind=kind,
            cell_grid=cell_grid,
            corrected_grid=cell_grid,
            bbox=origin_bbox,
            flavor=flavor,
            camelot_accuracy=accuracy,
        )

    # Cells the agent combined resolve by the source ADDRESSES it read off the
    # printed coordinate frame: address -> cell box, text-validated. The
    # grounding stage then closes the whole table: corrected_grid gives every
    # corrected cell its measured box, so nothing downstream re-derives geometry.
    merged: List[MergedCellBox] = []
    if correction.cell_merges:
        merged = resolve_merges(correction.cell_merges, cell_grid, correction.markdown)
        log_ungrounded(merged, page)
    corrected_grid = resolve_corrected_grid(
        correction.markdown, merged, cell_grid, correction.footnote_refs, correction.region_text
    )
    # Classified statuses are text-evidence hypotheses; the inspector verifies
    # the inspected ones against the table image, and whatever it cannot
    # positively confirm downgrades to `unverified` for user inspection. With
    # no inspector configured, the proposed statuses stand unconfirmed.
    await inspect_gap_cells(
        corrected_grid,
        correction.markdown,
        str(assembly.source),
        page,
        page_image,
        bbox,
        assembly.dpi,
        assembly.inspector,
    )
    log_ungrounded_cells(corrected_grid, page)
    dropped_text = find_dropped_header_text(
        corrected_grid,
        cell_grid,
        " ".join(
            [correction.title, correction.caption, correction.units]
            + [f"{f.marker} {f.text}".strip() for f in correction.footnotes]
        ),
        page,
    )
    return ExtractedTable(
        table_id=table_id,
        content_fingerprint=fingerprint,
        title=correction.title,
        caption=correction.caption,
        markdown=correction.markdown,
        footnotes=correction.footnotes,
        footnote_refs=correction.footnote_refs,
        footnote_marks=locate_markers(
            correction.footnote_marks,
            correction.footnote_refs,
            correction.markdown,
            correction.footnotes,
            # Both titles: the correction sometimes strips a marker suffix the
            # image-read title retains.
            f"{correction.title} {title} {correction.caption}",
        ),
        units=correction.units,
        header_rows=correction.header_rows,
        llm_corrected=correction.llm_corrected,
        corrected_grid=corrected_grid,
        merged_cells=merged,
        dropped_text=dropped_text,
        page=page,
        source=str(assembly.source),
        som_region=som_region,
        kind=kind,
        cell_grid=cell_grid,
        bbox=origin_bbox,
        flavor=flavor,
        camelot_accuracy=accuracy,
    )
