#!/usr/bin/env python3
"""Build two synthetic PDFs that differ only in page text and bookmark titles."""

from __future__ import annotations

import csv
import hashlib
import json
from io import BytesIO
from pathlib import Path

import pypdf
import reportlab
from pypdf import PdfReader, PdfWriter
from pypdf.generic import DictionaryObject, Fit, NameObject, TextStringObject
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas


ROOT = Path(__file__).resolve().parent
SOURCE = ROOT / "source-bookmarks.pdf"
TARGET = ROOT / "translated-bookmarks.pdf"
AUDIT = ROOT / "bookmark-destination-audit.csv"
MANIFEST = ROOT / "fixture-manifest.json"


SOURCE_PAGES = [
    ("Introduction", "This synthetic page anchors the first bookmark."),
    ("Method", "The Method bookmark uses a named destination."),
    ("Results", "This bookmark preserves a FitH page destination."),
    ("Appendix", "The final top-level bookmark opens this page."),
]

TARGET_PAGES = [
    ("Introducción", "Esta página sintética contiene el primer destino."),
    ("Método", "El marcador Método usa un destino con nombre."),
    ("Resultados", "Este marcador conserva un destino FitH."),
    ("Apéndice", "El último marcador principal abre esta página."),
]

SOURCE_BOOKMARKS = [
    (0, "Field Guide", "GoTo page", "page:1:Fit"),
    (1, "Introduction", "GoTo page", "page:1:Fit"),
    (1, "Method", "GoTo named destination", "named:method->page:2:FitH:top=826"),
    (1, "Results", "GoTo page", "page:3:FitH:top=720"),
    (1, "Project page", "URI", "URI:https://www.booktranslator.app/"),
    (0, "Appendix", "GoTo page", "page:4:Fit"),
]

TARGET_TITLES = [
    "Guía de campo",
    "Introducción",
    "Método",
    "Resultados",
    "Página del proyecto",
    "Apéndice",
]


def page_pdf(pages: list[tuple[str, str]]) -> BytesIO:
    stream = BytesIO()
    doc = canvas.Canvas(stream, pagesize=letter, invariant=1)
    for index, (heading, body) in enumerate(pages, start=1):
        doc.setTitle("Synthetic PDF Bookmark Fixture")
        doc.setFont("Helvetica-Bold", 22)
        doc.drawString(72, 720, heading)
        doc.setFont("Helvetica", 11)
        doc.drawString(72, 690, body)
        doc.drawString(72, 665, f"Synthetic fixture page {index}; not a BookTranslator output.")
        doc.showPage()
    doc.save()
    stream.seek(0)
    return stream


def uri_outline(title: str, uri: str) -> DictionaryObject:
    action = DictionaryObject(
        {
            NameObject("/S"): NameObject("/URI"),
            NameObject("/URI"): TextStringObject(uri),
        }
    )
    return DictionaryObject(
        {
            NameObject("/Title"): TextStringObject(title),
            NameObject("/A"): action,
        }
    )


def named_outline(title: str, destination: str) -> DictionaryObject:
    action = DictionaryObject(
        {
            NameObject("/S"): NameObject("/GoTo"),
            NameObject("/D"): TextStringObject(destination),
        }
    )
    return DictionaryObject(
        {
            NameObject("/Title"): TextStringObject(title),
            NameObject("/A"): action,
        }
    )


def build(output: Path, pages: list[tuple[str, str]], titles: list[str]) -> None:
    reader = PdfReader(page_pdf(pages))
    writer = PdfWriter()
    writer.append_pages_from_reader(reader)
    writer.add_named_destination("method", 1)

    root = writer.add_outline_item(titles[0], 0, is_open=True)
    writer.add_outline_item(titles[1], 0, parent=root)
    writer.add_outline_item_dict(named_outline(titles[2], "method"), parent=root)
    writer.add_outline_item(
        titles[3], 2, parent=root, fit=Fit.fit_horizontally(top=720)
    )
    writer.add_outline_item_dict(
        uri_outline(titles[4], "https://www.booktranslator.app/"), parent=root
    )
    writer.add_outline_item(titles[5], 3)

    writer.add_metadata(
        {
            "/Title": "Synthetic PDF Bookmark Fixture",
            "/Subject": "CC0 test fixture; not a BookTranslator product output",
        }
    )
    with output.open("wb") as handle:
        writer.write(handle)


def sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def number_label(value: object) -> str:
    number = float(value)
    return str(int(number)) if number.is_integer() else str(number)


def page_destination_fingerprint(
    page_number: int, fit_type: object, operands: list[object]
) -> str:
    fit_name = str(fit_type).removeprefix("/")
    operand_labels = {
        "XYZ": ["left", "top", "zoom"],
        "FitH": ["top"],
        "FitV": ["left"],
        "FitR": ["left", "bottom", "right", "top"],
        "FitBH": ["top"],
        "FitBV": ["left"],
    }.get(fit_name, [])
    serialized = [f"page:{page_number}:{fit_name}"]
    for index, operand in enumerate(operands):
        label = operand_labels[index] if index < len(operand_labels) else f"arg{index + 1}"
        value = "null" if operand is None else number_label(operand)
        serialized.append(f"{label}={value}")
    return ":".join(serialized)


def named_destination_contract(reader: PdfReader) -> dict[str, str]:
    result: dict[str, str] = {}
    attribute_order = {
        "/XYZ": ["left", "top", "zoom"],
        "/FitH": ["top"],
        "/FitV": ["left"],
        "/FitR": ["left", "bottom", "right", "top"],
        "/FitBH": ["top"],
        "/FitBV": ["left"],
    }
    for name, destination in reader.named_destinations.items():
        page_number = reader.get_destination_page_number(destination) + 1
        operands = [getattr(destination, attribute) for attribute in attribute_order.get(destination.typ, [])]
        result[name] = page_destination_fingerprint(page_number, destination.typ, operands)
    return result


def outline_contract(path: Path) -> list[tuple[int, str, str]]:
    reader = PdfReader(path)
    named_destinations = named_destination_contract(reader)
    page_numbers = {
        page.indirect_reference.idnum: index + 1
        for index, page in enumerate(reader.pages)
        if page.indirect_reference is not None
    }
    rows: list[tuple[int, str, str]] = []

    def walk(node: DictionaryObject, depth: int = 0) -> None:
        child = node.get("/First")
        while child is not None:
            item = child.get_object()
            action = item.get("/A")
            action_type = action.get("/S") if action is not None else None
            destination = action.get("/D") if action is not None else None
            if action_type == "/URI":
                fingerprint = f"URI:{action.get('/URI')}"
            elif isinstance(destination, str):
                fingerprint = (
                    f"named:{destination}->{named_destinations.get(destination, 'unresolved')}"
                )
            elif destination:
                page = destination[0]
                page_number = page_numbers.get(page.idnum, 0)
                fingerprint = page_destination_fingerprint(
                    page_number, destination[1], list(destination[2:])
                )
            else:
                fingerprint = "unknown"
            rows.append((depth, str(item.get("/Title")), fingerprint))
            if item.get("/First") is not None:
                walk(item, depth + 1)
            child = item.get("/Next")

    walk(reader.trailer["/Root"]["/Outlines"])
    return rows


def main() -> None:
    build(SOURCE, SOURCE_PAGES, [row[1] for row in SOURCE_BOOKMARKS])
    build(TARGET, TARGET_PAGES, TARGET_TITLES)

    source_contract = outline_contract(SOURCE)
    target_contract = outline_contract(TARGET)
    expected_source_contract = [
        (depth, title, destination)
        for depth, title, _, destination in SOURCE_BOOKMARKS
    ]
    if source_contract != expected_source_contract:
        raise ValueError(
            f"Generated source contract differs from the declared fixture: {source_contract}"
        )
    if len(source_contract) != len(target_contract):
        raise ValueError("Source and translated outlines contain different item counts")

    audit_rows: list[list[object]] = []
    for source, target, declared in zip(
        source_contract, target_contract, SOURCE_BOOKMARKS, strict=True
    ):
        source_depth, source_title, source_destination = source
        target_depth, target_title, target_destination = target
        action = declared[2]
        passed = source_depth == target_depth and source_destination == target_destination
        audit_rows.append(
            [
                source_depth,
                source_title,
                target_title,
                action,
                source_destination,
                target_destination,
                "PASS" if passed else "FAIL",
            ]
        )
    if any(row[-1] != "PASS" for row in audit_rows):
        raise ValueError(f"Source and translated destination contracts differ: {audit_rows}")

    with AUDIT.open("w", newline="", encoding="utf-8") as handle:
        writer = csv.writer(handle, lineterminator="\n")
        writer.writerow(
            [
                "depth",
                "source_title",
                "translated_title",
                "action_type",
                "source_destination",
                "translated_destination",
                "audit_result",
            ]
        )
        writer.writerows(audit_rows)

    source_reader = PdfReader(SOURCE)
    target_reader = PdfReader(TARGET)
    source_named_destinations = named_destination_contract(source_reader)
    target_named_destinations = named_destination_contract(target_reader)
    manifest = {
        "license": "CC0-1.0",
        "disclaimer": "Synthetic fixture authored for this article; not a BookTranslator product output.",
        "generator": Path(__file__).name,
        "dependencies": {
            "pypdf": pypdf.__version__,
            "reportlab": reportlab.Version,
        },
        "files": {
            SOURCE.name: {"sha256": sha256(SOURCE), "pages": 4, "bookmarks": 6},
            TARGET.name: {"sha256": sha256(TARGET), "pages": 4, "bookmarks": 6},
            AUDIT.name: {"sha256": sha256(AUDIT), "rows": 6},
        },
        "checks": {
            "page_count_equal": len(PdfReader(SOURCE).pages) == len(PdfReader(TARGET).pages),
            "source_named_destination": "method" in source_named_destinations,
            "translated_named_destination": "method" in target_named_destinations,
            "named_destination_targets_match": source_named_destinations
            == target_named_destinations,
            "outline_depth_and_action_match": [
                (depth, action) for depth, _, action in source_contract
            ]
            == [(depth, action) for depth, _, action in target_contract],
            "outline_titles_changed": [title for _, title, _ in source_contract]
            != [title for _, title, _ in target_contract],
            "audit_pass_rows": sum(row[-1] == "PASS" for row in audit_rows),
        },
    }
    MANIFEST.write_text(f"{json.dumps(manifest, indent=2)}\n", encoding="utf-8")


if __name__ == "__main__":
    main()
