"""Verify every gap cell's proposed status against the table image.

The classifier assigns each unboxed corrected cell a condition from text
evidence alone — a hypothesis. This stage has the status inspector LOOK at
the table crop and give a three-way verdict per cell, and holds authority
deterministically: a condition the inspector positively confirms keeps its
status with the evidence recorded on the cell; a condition the image
positively CONTRADICTS (the page shows something else there) becomes
`defect`; anything the image cannot settle — including unmentioned cells
and unreachable inspections (failed call, missing crop) — becomes
`unverified`. The inspector is never forced to pick the nearest option when
none fits. There is no upgrade path anywhere: inspection can move a cell
into the review or defect queues, never out of them.
"""

from __future__ import annotations

import asyncio
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple

from loguru import logger

from quber.agents.completeness import page_words
from quber.agents.status_inspector import StatusInspector
from quber.core.extractors.base import GroundedCell
from quber.core.extractors.camelot.correspondence.geometry import crop_region_png, table_crop_box

# The statuses that are hypotheses needing visual confirmation. `reconciled`
# has its box; `single_character` is definitional (text length); everything
# else claims something only the image can show.
INSPECTED_STATUSES = frozenset(
    {
        "header_printed_unlocated",
        "label_printed_unlocated",
        "total_label_added",
        "header_label_added",
        "unverified",
    }
)

# The header/label split is a diagnostic convenience — both are pass-tier —
# so both carry the SAME position-agnostic condition: the claim to verify is
# that the text is printed on the page, not where. A position-specific
# framing let a row-kind misclassification turn a correctly printed label
# into a false defect.
# The condition states only what the IMAGE can verify. Naming the internal
# mechanism ("could not be located in the text grid") invited the model to
# dispute that unverifiable clause and contradict text it could see printed.
_PRINTED_CONDITION = (
    "this text is printed on the page — as a header, a band, or a row label; "
    "it may wrap across printed lines, combine fragments printed apart, or "
    "share a line with neighboring text"
)
_CONDITION_TEXT = {
    "header_printed_unlocated": _PRINTED_CONDITION,
    "label_printed_unlocated": _PRINTED_CONDITION,
    "total_label_added": (
        "the extraction ADDED this conventional label to a row printed without "
        "one; the page prints nothing at this position and the row visibly "
        "totals or summarizes its section"
    ),
    "header_label_added": (
        "the extraction ADDED this conventional column heading; the page prints "
        "the table without a header over this column, so nothing is printed at "
        "this position"
    ),
    "unverified": (
        "the extraction wrote this text without a known printed source. This "
        "condition holds when the page prints NOTHING at the position (or the "
        "text is nowhere to be seen); it is contradicted ONLY when the page "
        "prints a DIFFERENT word at the position that the extraction should "
        "have used instead"
    ),
}


async def inspect_gap_cells(
    corrected_grid: Sequence[Sequence[GroundedCell]],
    markdown: str,
    source: str,
    page: int,
    page_image: Optional[Path],
    bbox: Optional[Tuple[float, float, float, float]],
    dpi: int,
    inspector: Optional[StatusInspector],
) -> None:
    """Run one inspection over the table's flagged cells and gate the results."""
    flagged: List[Tuple[int, int, GroundedCell]] = [
        (r, c, cell)
        for r, row in enumerate(corrected_grid)
        for c, cell in enumerate(row)
        if cell.status in INSPECTED_STATUSES
    ]
    if not flagged or inspector is None:
        return
    if page_image is None or bbox is None:
        _downgrade_all(flagged, "no table image available for inspection", page)
        return

    try:
        _pw, page_h, _words = await asyncio.to_thread(page_words, Path(source), page)
        crop_png = await asyncio.to_thread(crop_region_png, page_image, table_crop_box(bbox, page_h), dpi)
    except Exception as exc:
        logger.warning(
            "page {}: could not prepare the table image for inspection; its flagged cells "
            "are recorded as unverified in the review flags: {}",
            page,
            exc,
        )
        _downgrade_all(flagged, "inspection crop failed", page)
        return

    payload: List[Dict[str, Any]] = [
        {"row": r, "col": c, "text": cell.text, "condition": _CONDITION_TEXT[cell.status or ""]}
        for r, c, cell in flagged
    ]
    report = await inspector.inspect(crop_png, markdown, payload, page)
    findings = {(f.row, f.col): f for f in report.findings} if report else {}

    for r, c, cell in flagged:
        f = findings.get((r, c))
        if f is None:
            _reassign(cell, "unverified", "not confirmed by inspection", page, r, c)
        elif f.verdict == "holds":
            cell.note = f.evidence
        elif f.verdict == "contradicted":
            _reassign(cell, "defect", f.evidence, page, r, c)
        else:
            _reassign(cell, "unverified", f.evidence, page, r, c)


def _reassign(cell: GroundedCell, status: str, evidence: str, page: int, r: int, c: int) -> None:
    # A confirmed defect is the one outcome a person must act on, so it alone
    # warns; downgrades to unverified reach the reviewer through the flags
    # record and leave only a debugging trace here.
    if cell.status != status:
        if status == "defect":
            logger.warning(
                "page {}: the page contradicts the output at row {}, col {} — the extraction "
                "wrote {!r}, but the inspector saw: {} (recorded as a defect in the review flags)",
                page,
                r,
                c,
                cell.text[:40],
                evidence,
            )
        else:
            logger.debug(
                "page {}: cell (row {}, col {}) {!r} could not be confirmed against the page ({} -> {}): {}",
                page,
                r,
                c,
                cell.text[:40],
                cell.status,
                status,
                evidence,
            )
    cell.status = status
    cell.note = evidence


def _downgrade_all(flagged: Sequence[Tuple[int, int, GroundedCell]], reason: str, page: int) -> None:
    for r, c, cell in flagged:
        _reassign(cell, "unverified", reason, page, r, c)
