"""sheet_reconcile.py - deterministic sheet-list reconciliation (no LLM).

The cover sheet's own sheet index (SHEET LIST / DRAWING INDEX) declares which
sheets the set is SUPPOSED to contain. Comparing that declaration against the
sheets wave-1 actually identified answers two early questions:

- declared_not_in_set: sheets the index lists but we didn't identify - dark
  pages, misidentification, or disciplines genuinely absent from this PDF.
- in_set_not_declared: sheet numbers we extracted that the index doesn't
  list - misread title blocks or unlisted sheets.

Deterministic complement to the LLM sheet_index stage, which can only infer
from what extraction already found.
"""

import re
from typing import Dict, List, Optional

# Markers that introduce the drawing set's own sheet index on a cover page.
_INDEX_MARKERS = (
    "SHEET LIST",
    "DRAWING INDEX",
    "SHEET INDEX",
    "DRAWING LIST",
    "INDEX OF DRAWINGS",
)

# Sheet ids: 1-2 letters, optional hyphen, 2-3 digits, optional decimal suffix.
# Covers S301, A102, LS101, C-001, C-001.1; excludes dates/project numbers
# (pure digits) and member marks (W12X26 - letter after digits).
_SHEET_TOKEN_RE = re.compile(r"\b([A-Z]{1,2}-?\d{2,3}(?:\.\d+)?)\b")

# Only cover-front pages carry the set index.
_MAX_INDEX_PAGE = 5


def _normalize_id(sheet_id: str) -> str:
    return (sheet_id or "").upper().replace("-", "").strip()


def declared_sheet_list(page_texts: Dict[int, Optional[str]]) -> List[str]:
    """Scrape the declared sheet list off the cover page's text layer.

    page_texts: {page_number: text_layer_or_None}. Returns the ordered,
    deduped list of declared sheet ids, or [] when no index marker exists.
    Only the FIRST page containing a marker is parsed (later 'sheet list'
    echoes in legends/schedules are ignored).
    """
    for page_number in sorted(page_texts):
        if page_number > _MAX_INDEX_PAGE:
            break
        text = page_texts.get(page_number) or ""
        upper = text.upper()
        marker_at = -1
        for marker in _INDEX_MARKERS:
            marker_at = upper.find(marker)
            if marker_at >= 0:
                break
        if marker_at < 0:
            continue
        section = text[marker_at:]
        declared: List[str] = []
        for token in _SHEET_TOKEN_RE.findall(section):
            if token not in declared:
                declared.append(token)
        return declared
    return []


def reconcile_sheets(sheets: List[Dict], declared: List[str]) -> Dict:
    """Compare extracted sheet_numbers against the declared index.

    Comparison is hyphen/case-normalized; output lists keep the declared /
    extracted originals.
    """
    found: List[str] = [str(s["sheet_number"]) for s in sheets or []
                        if s.get("sheet_number")]
    found_norm = {_normalize_id(n) for n in found}
    declared_norm = {_normalize_id(n) for n in declared}

    declared_not_in_set = [n for n in declared if _normalize_id(n) not in found_norm]
    # Preserve extraction order, dedupe, keep originals.
    in_set_not_declared: List[str] = []
    for n in found:
        if _normalize_id(n) not in declared_norm and n not in in_set_not_declared:
            in_set_not_declared.append(n)

    return {
        "declared_total": len(declared),
        "found_total": len(found),
        "declared_not_in_set": declared_not_in_set,
        "in_set_not_declared": in_set_not_declared,
    }
