from __future__ import annotations

import csv
import hashlib
import json
import os
import random
import shutil
import subprocess
import sys
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile, ZipInfo

import PIL
from PIL import Image, ImageDraw, ImageFilter, ImageFont


ROOT = Path(__file__).parent
PAGES = ROOT / "page-images"
OCR = ROOT / "raw-ocr"
OUTPUT = ROOT / "lantern-ledger-scan-to-epub.epub"
FONT_PATH = Path(
    os.environ.get(
        "BOOK_FIXTURE_FONT",
        "/System/Library/Fonts/Supplemental/Georgia.ttf",
    )
)
FIXED_TIME = (2026, 9, 19, 0, 0, 0)

TITLE = "The Lantern Ledger"
CHAPTERS = [
    (
        "chapter-01.xhtml",
        "Chapter One: The Quay",
        [
            "At first light, Mara carried the brass lantern to the empty quay. The tide had erased every footprint except one narrow trail beside the warehouse door.",
            "She opened the ledger and found a name written twice: once in black ink, once in blue. The difference looked small, but the harbour master treated it as a warning.",
        ],
    ),
    (
        "chapter-02.xhtml",
        "Chapter Two: The Inventory",
        [
            "The lower shelf held three maps, a cracked compass, and a sealed packet. Mara recorded each object before moving it, because order mattered more than speed.",
            "A page torn from the back of the ledger listed a destination beyond the northern shoal. The final line stopped after the word river, as if the writer had been interrupted.",
        ],
    ),
    (
        "chapter-03.xhtml",
        "Chapter Three: The Crossing",
        [
            "By noon the fog had thinned. Mara followed the marked channel and kept the lantern covered until the boat passed the last buoy.",
            "She did not know whether the repeated name described one traveller or two. She knew only that the same uncertainty should not multiply in every later copy.",
        ],
    ),
    (
        "chapter-04.xhtml",
        "Chapter Four: The Return",
        [
            "The packet contained no treasure, only a corrected manifest and a short note. The blue name was the approved form; the black name had survived from an earlier draft.",
            "Mara returned the ledger before dusk. She left the lantern on the quay, where the next reader could begin with the right record instead of repeating her search.",
        ],
    ),
]

MIMETYPE = "application/epub+zip"
CONTAINER = """<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
  <rootfiles><rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/></rootfiles>
</container>
"""
STYLE = "body { font-family: serif; line-height: 1.55; margin: 5%; } h1 { break-before: page; }"


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


def preflight() -> None:
    if not FONT_PATH.is_file():
        raise SystemExit(
            "Missing fixture font. Set BOOK_FIXTURE_FONT to a readable TrueType "
            "font path; the committed artifact used macOS Georgia.ttf."
        )
    if not shutil.which("tesseract"):
        raise SystemExit(
            "Missing tesseract executable. Install Tesseract 5.5.3 with the eng "
            "traineddata package, or place a compatible tesseract on PATH."
        )
    languages = subprocess.run(
        ["tesseract", "--list-langs"],
        check=True,
        capture_output=True,
        text=True,
    ).stdout.splitlines()
    if "eng" not in {language.strip() for language in languages}:
        raise SystemExit(
            "Missing Tesseract eng traineddata. Install it before running the fixture."
        )


def wrap(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.FreeTypeFont, width: int) -> list[str]:
    words = text.split()
    lines: list[str] = []
    current: list[str] = []
    for word in words:
        candidate = " ".join([*current, word])
        if draw.textlength(candidate, font=font) <= width:
            current.append(word)
        else:
            lines.append(" ".join(current))
            current = [word]
    if current:
        lines.append(" ".join(current))
    return lines


def render_pages() -> list[Path]:
    PAGES.mkdir(parents=True, exist_ok=True)
    body = ImageFont.truetype(str(FONT_PATH), 38)
    heading = ImageFont.truetype(str(FONT_PATH), 50)
    small = ImageFont.truetype(str(FONT_PATH), 25)
    created: list[Path] = []
    random.seed(20260919)
    for index, (_, chapter, paragraphs) in enumerate(CHAPTERS, 1):
        image = Image.new("L", (1200, 1600), 243)
        pixels = image.load()
        for y in range(image.height):
            for x in range(image.width):
                pixels[x, y] = max(224, min(252, pixels[x, y] + random.choice((-2, -1, 0, 0, 0, 1, 2))))
        draw = ImageDraw.Draw(image)
        draw.text((90, 55), TITLE.upper(), font=small, fill=92)
        draw.line((90, 95, 1110, 95), fill=170, width=2)
        y = 175
        for line in wrap(draw, chapter, heading, 980):
            draw.text((110, y), line, font=heading, fill=38)
            y += 68
        y += 45
        for paragraph in paragraphs:
            lines = wrap(draw, paragraph, body, 960)
            # Force one source line break with a printed hyphen to demonstrate that
            # OCR output requires editorial reconstruction before EPUB packaging.
            if index == 2 and paragraph == paragraphs[1] and len(lines) >= 3:
                lines[1] = lines[1].rstrip() + "-"
            for line in lines:
                draw.text((120, y), line, font=body, fill=45)
                y += 55
            y += 35
        draw.text((585, 1515), str(index), font=small, fill=95)
        image = image.rotate((-0.35, 0.25, -0.2, 0.3)[index - 1], resample=Image.Resampling.BICUBIC, fillcolor=243)
        image = image.filter(ImageFilter.GaussianBlur(0.25))
        path = PAGES / f"page-{index:02d}.png"
        image.save(path, optimize=True)
        created.append(path)
    return created


def run_ocr(pages: list[Path]) -> list[Path]:
    OCR.mkdir(parents=True, exist_ok=True)
    outputs: list[Path] = []
    for page in pages:
        base = OCR / page.stem
        subprocess.run(
            ["tesseract", str(page), str(base), "-l", "eng", "--oem", "1", "--psm", "6", "--dpi", "300"],
            check=True,
            capture_output=True,
            text=True,
        )
        outputs.append(base.with_suffix(".txt"))
    return outputs


def xhtml(title: str, paragraphs: list[str]) -> str:
    body = "".join(f"<p>{paragraph}</p>" for paragraph in paragraphs)
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head><title>{title}</title><link rel="stylesheet" type="text/css" href="style.css"/></head>
<body><h1>{title}</h1>{body}</body>
</html>
"""


def write_entry(book: ZipFile, name: str, data: str, compression: int) -> None:
    info = ZipInfo(name, FIXED_TIME)
    info.compress_type = compression
    info.external_attr = 0o644 << 16
    book.writestr(info, data.encode("utf-8"))


def build_epub() -> None:
    nav_items = "".join(f'<li><a href="{name}">{title}</a></li>' for name, title, _ in CHAPTERS)
    nav = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="en" lang="en">
<head><title>Contents</title></head><body><nav epub:type="toc" id="toc"><h1>Contents</h1><ol>{nav_items}</ol></nav></body>
</html>
"""
    manifest = "\n".join(f'    <item id="c{i}" href="{name}" media-type="application/xhtml+xml"/>' for i, (name, _, _) in enumerate(CHAPTERS, 1))
    spine = "\n".join(f'    <itemref idref="c{i}"/>' for i in range(1, len(CHAPTERS) + 1))
    package = f"""<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id" xml:lang="en">
  <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:identifier id="pub-id">urn:uuid:1ae30ac7-b1ce-47fd-9789-f4374a830f5f</dc:identifier>
    <dc:title>{TITLE}</dc:title><dc:creator>BookTranslator Test Fixture</dc:creator><dc:language>en</dc:language>
    <dc:rights>CC0 1.0 Universal</dc:rights>
    <meta property="dcterms:modified">2026-09-19T00:00:00Z</meta>
  </metadata>
  <manifest>
    <item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
    <item id="css" href="style.css" media-type="text/css"/>
{manifest}
  </manifest>
  <spine>
{spine}
  </spine>
</package>
"""
    with ZipFile(OUTPUT, "w") as book:
        write_entry(book, "mimetype", MIMETYPE, ZIP_STORED)
        write_entry(book, "META-INF/container.xml", CONTAINER, ZIP_DEFLATED)
        write_entry(book, "EPUB/package.opf", package, ZIP_DEFLATED)
        write_entry(book, "EPUB/nav.xhtml", nav, ZIP_DEFLATED)
        write_entry(book, "EPUB/style.css", STYLE, ZIP_DEFLATED)
        for name, title, paragraphs in CHAPTERS:
            write_entry(book, f"EPUB/{name}", xhtml(title, paragraphs), ZIP_DEFLATED)


def write_tracking(ocr_files: list[Path]) -> None:
    rows: list[dict[str, str | int]] = []
    for index, ocr_file in enumerate(ocr_files, 1):
        lines = [line for line in ocr_file.read_text().splitlines() if line.strip()]
        rows.extend(
            [
                {
                    "page": index,
                    "error_class": "running_head",
                    "raw_ocr": lines[0],
                    "source_text": TITLE.upper(),
                    "epub_action": "remove",
                    "reason": "repeated print page furniture, not chapter content",
                },
                {
                    "page": index,
                    "error_class": "page_number",
                    "raw_ocr": lines[-1],
                    "source_text": str(index),
                    "epub_action": "remove",
                    "reason": "retain page mapping separately; do not insert numeral in prose",
                },
            ]
        )
    page_two = ocr_files[1].read_text()
    raw_hyphen = "line-\\nstopped" if "line-\nstopped" in page_two else "not reproduced"
    rows.append(
        {
            "page": 2,
            "error_class": "line_end_hyphenation",
            "raw_ocr": raw_hyphen,
            "source_text": "line stopped",
            "epub_action": "join with a space after checking the source image",
            "reason": "the printed hyphen marks a line wrap, not a lexical compound",
        }
    )
    with (ROOT / "error-tracking.csv").open("w", newline="") as handle:
        writer = csv.DictWriter(
            handle,
            fieldnames=rows[0].keys(),
            lineterminator="\n",
        )
        writer.writeheader()
        writer.writerows(rows)


preflight()
pages = render_pages()
ocr_files = run_ocr(pages)
build_epub()
write_tracking(ocr_files)

manifest = {
    "generated_at": "2026-09-19T00:00:00Z",
    "license": "Fixture text, generator, and generated artifacts are dedicated to the public domain under CC0 1.0.",
    "python_version": sys.version.split()[0],
    "pillow_version": PIL.__version__,
    "tesseract_version": subprocess.run(["tesseract", "--version"], check=True, capture_output=True, text=True).stdout.splitlines()[0],
    "ocr_settings": "eng; OEM 1; PSM 6; 300 dpi",
    "font": {"path": str(FONT_PATH), "sha256": sha256(FONT_PATH)},
    "pages": [{"path": str(path.relative_to(ROOT)), "sha256": sha256(path)} for path in pages],
    "raw_ocr": [{"path": str(path.relative_to(ROOT)), "sha256": sha256(path)} for path in ocr_files],
    "epub": {"path": OUTPUT.name, "sha256": sha256(OUTPUT)},
}
(ROOT / "fixture-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(json.dumps(manifest, indent=2))
