import argparse
from dataclasses import dataclass
from datetime import date
from hashlib import sha256
from io import BytesIO
from pathlib import Path
import re

import pdfplumber

HEADERS = ("Equipment ID", "Equipment", "Loan limit", "Conditions")


@dataclass(frozen=True)
class EquipmentRow:
    page: int
    table: int
    row: int
    cells: tuple[str, ...]


@dataclass(frozen=True)
class Edition:
    filename: str
    effective: date
    digest: str
    rows: dict[str, EquipmentRow]


def read_edition(path):
    data = path.read_bytes()
    rows = {}
    with pdfplumber.open(BytesIO(data)) as pdf:
        first_page = pdf.pages[0].extract_text() if pdf.pages else ""
        match = re.search(r"^Effective ([0-9]{4}-[0-9]{2}-[0-9]{2})$", first_page or "", re.M)
        if not match:
            raise ValueError(f"{path.name}: expected an effective date on page 1.")
        effective = date.fromisoformat(match[1])
        for page in pdf.pages:
            for table_number, table in enumerate(page.extract_tables(), start=1):
                headers = tuple(" ".join((cell or "").split()) for cell in table[0])
                if headers != HEADERS:
                    raise ValueError(f"{path.name}, page {page.page_number}: unexpected table headers.")
                for row_number, row in enumerate(table[1:], start=2):
                    cells = tuple(" ".join((cell or "").split()) for cell in row)
                    if len(cells) != len(HEADERS) or not all(cells):
                        raise ValueError(f"{path.name}, page {page.page_number}: incomplete row {row_number}.")
                    equipment_id = cells[0]
                    if equipment_id in rows:
                        raise ValueError(f"{path.name}: repeated ID {equipment_id}.")
                    rows[equipment_id] = EquipmentRow(page.page_number, table_number, row_number, cells)
    if not rows:
        raise ValueError(f"{path.name}: no equipment rows were extracted.")
    return Edition(path.name, effective, sha256(data).hexdigest(), rows)


def print_row(edition, row):
    print(f"{edition.filename} | effective {edition.effective} | page {row.page}, table {row.table}, row {row.row}")
    for column, value in zip(HEADERS, row.cells):
        print(f"  {column}: {value}")


def lookup(folder, equipment_id, as_of):
    paths = sorted(folder.glob("equipment-policy-*.pdf"))
    if not paths:
        raise ValueError(f"No equipment-policy PDFs found in {folder}.")
    editions = [read_edition(path) for path in paths]
    dates = {edition.effective for edition in editions}
    if len(dates) != len(editions):
        raise ValueError("More than one source has the same effective date. Review the editions.")
    active = max((edition for edition in editions if edition.effective <= as_of),
                 key=lambda edition: edition.effective, default=None)
    if active is None:
        print(f"No edition is effective on {as_of}.")
        return
    row = active.rows.get(equipment_id)
    if row is None:
        print(f"{equipment_id} is absent from {active.filename} (effective {active.effective}).")
        return
    print_row(active, row)
    print(f"  File SHA-256: {active.digest}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Inspect and search a fictional equipment policy.")
    commands = parser.add_subparsers(dest="command", required=True)
    inspect = commands.add_parser("inspect")
    inspect.add_argument("pdf", type=Path)
    find = commands.add_parser("find")
    find.add_argument("folder", type=Path)
    find.add_argument("equipment_id", type=str.upper)
    find.add_argument("--as-of", type=date.fromisoformat, required=True)
    args = parser.parse_args()

    try:
        if args.command == "inspect":
            edition = read_edition(args.pdf)
            for row in edition.rows.values():
                print_row(edition, row)
        else:
            lookup(args.folder, args.equipment_id, args.as_of)
    except (OSError, ValueError, pdfplumber.utils.exceptions.PdfminerException) as error:
        raise SystemExit(str(error)) from None
