"""Reject corrupt inputs before they reach OMRChecker."""

from __future__ import annotations

from dataclasses import asdict, dataclass
from pathlib import Path

import fitz
from PIL import Image, UnidentifiedImageError


CORRUPT_FILE = "CORRUPT_OR_INCOMPLETE_FILE"


@dataclass(frozen=True)
class ValidationIssue:
    code: str
    filename: str
    page_number: int | None
    detail: str

    def as_dict(self) -> dict[str, object]:
        return asdict(self)


def corrupt(path: Path, detail: str, page_number: int | None = None) -> ValidationIssue:
    return ValidationIssue(CORRUPT_FILE, path.name, page_number, detail)


def validate_image(path: Path) -> ValidationIssue | None:
    """Fully decode an image; reading its header alone is insufficient."""
    if not path.is_file() or path.stat().st_size == 0:
        return corrupt(path, "File is missing or empty")
    try:
        with Image.open(path) as image:
            image.verify()
        with Image.open(path) as image:
            image.load()
            if image.width < 1 or image.height < 1:
                return corrupt(path, "Image has invalid dimensions")
    except (OSError, SyntaxError, ValueError, UnidentifiedImageError) as error:
        return corrupt(path, f"Image cannot be fully decoded: {error}")
    return None


def partition_images(paths: list[Path]) -> tuple[list[Path], list[ValidationIssue]]:
    """Keep valid images in input order and return corrupt items separately."""
    valid: list[Path] = []
    issues: list[ValidationIssue] = []
    for path in paths:
        issue = validate_image(path)
        if issue:
            issues.append(issue)
        else:
            valid.append(path)
    return valid, issues


def has_pdf_eof(path: Path) -> bool:
    if not path.is_file() or path.stat().st_size == 0:
        return False
    with path.open("rb") as handle:
        handle.seek(max(path.stat().st_size - 4096, 0))
        return b"%%EOF" in handle.read()


def open_pdf(path: Path) -> tuple[fitz.Document | None, ValidationIssue | None]:
    """Open a structurally complete PDF without accepting a truncated tail."""
    if not has_pdf_eof(path):
        return None, corrupt(path, "PDF is missing its end-of-file marker")
    try:
        document = fitz.open(path)
        if document.page_count < 1:
            document.close()
            return None, corrupt(path, "PDF contains no pages")
        return document, None
    except (fitz.FileDataError, RuntimeError, ValueError) as error:
        return None, corrupt(path, f"PDF cannot be opened: {error}")


def render_pdf_pages(
    path: Path, destination: Path, dpi: int
) -> tuple[list[dict[str, object]], list[ValidationIssue]]:
    """Render every readable page while retaining exact rejected page numbers."""
    document, issue = open_pdf(path)
    if issue:
        return [], [issue]
    assert document is not None
    matrix = fitz.Matrix(dpi / 72, dpi / 72)
    page_map: list[dict[str, object]] = []
    issues: list[ValidationIssue] = []
    try:
        for index in range(document.page_count):
            page_number = index + 1
            filename = f"page-{page_number:04d}.png"
            try:
                page = document.load_page(index)
                pixmap = page.get_pixmap(matrix=matrix, colorspace=fitz.csGRAY, alpha=False)
                pixmap.save(destination / filename)
                page_map.append({"page": page_number, "file_id": filename})
            except (fitz.FileDataError, RuntimeError, ValueError, OSError) as error:
                issues.append(corrupt(path, f"PDF page cannot be rendered: {error}", page_number))
    finally:
        document.close()
    return page_map, issues
