#!/usr/bin/env python3
"""Build the controlled Apeiron publication milestone dossier as a PDF."""

from __future__ import annotations

import hashlib
import html
import re
from pathlib import Path

from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import mm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
    BaseDocTemplate,
    Frame,
    KeepTogether,
    LongTable,
    PageBreak,
    PageTemplate,
    Paragraph,
    Spacer,
    Table,
    TableStyle,
)


ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "APEIRON_PUBLICATION_MASTER_DOSSIER_LATEST.pdf"

CLAIMS = ROOT / "APEIRON_PUBLICATION_CLAIMS_REGISTER_LATEST.md"
MANUSCRIPT = ROOT / "APEIRON_MANUSCRIPT_LATEST.md"
GAPS = ROOT / "APEIRON_PUBLICATION_GAP_REPORT_LATEST.md"
MODEL_SPEC = ROOT / "APEIRON_CANONICAL_MODEL_SPECIFICATION_LATEST.md"
REPRO_README = ROOT / "README_REPRODUCIBILITY.md"
GATE_SPEC = ROOT / "APEIRON_V7_13_GATE_SPECIFICATION_LATEST.json"
REPRO_MANIFEST = ROOT / "APEIRON_REPRODUCIBILITY_MANIFEST_LATEST.json"
REPRO_PACKAGE = ROOT / "APEIRON_PUBLICATION_REPRODUCIBILITY_PACKAGE_LATEST.zip"

NAVY = colors.HexColor("#0B1F33")
TEAL = colors.HexColor("#0F9D8A")
MINT = colors.HexColor("#DFF5EF")
INK = colors.HexColor("#17324D")
MUTED = colors.HexColor("#5E7082")
RULE = colors.HexColor("#D6E0E8")
PALE = colors.HexColor("#F4F7F9")
AMBER = colors.HexColor("#F5B942")
RED = colors.HexColor("#C84C4C")


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as stream:
        for block in iter(lambda: stream.read(1024 * 1024), b""):
            h.update(block)
    return h.hexdigest()


def register_fonts() -> None:
    font_root = Path("/usr/share/fonts/truetype/dejavu")
    pdfmetrics.registerFont(TTFont("DejaVu", str(font_root / "DejaVuSans.ttf")))
    pdfmetrics.registerFont(TTFont("DejaVu-Bold", str(font_root / "DejaVuSans-Bold.ttf")))
    pdfmetrics.registerFont(TTFont("DejaVu-Mono", str(font_root / "DejaVuSansMono.ttf")))


def normalize_dashes(text: str) -> str:
    return (
        text.replace("\u2011", "-")
        .replace("\u2010", "-")
        .replace("\u2012", "-")
        .replace("\u2013", "-")
        .replace("\u2014", "-")
        .replace("\u2212", "-")
    )


LATEX_REPLACEMENTS = {
    r"\tau": "τ",
    r"\sigma": "σ",
    r"\mu": "μ",
    r"\eta": "η",
    r"\Sigma": "Σ",
    r"\Delta": "Δ",
    r"\kappa": "κ",
    r"\theta": "θ",
    r"\infty": "∞",
    r"\times": "×",
    r"\perp": "⊥",
    r"\langle": "⟨",
    r"\rangle": "⟩",
    r"\sqrt": "sqrt",
    r"\le": "≤",
    r"\ge": "≥",
    r"\ldots": "…",
    r"\rm": "",
    r"\varnothing": "∅",
    r"\Longleftrightarrow": "⇔",
    r"\bigwedge": "∧",
    r"\mid": "|",
    r"\land": "∧",
    r"\left": "",
    r"\right": "",
    r"\,": " ",
    r"\;": " ",
    r"\!": "",
}


def latex_to_text(value: str) -> str:
    value = value.replace("\\[", "").replace("\\]", "")
    value = value.replace("\\(", "").replace("\\)", "")
    for source, target in LATEX_REPLACEMENTS.items():
        value = value.replace(source, target)
    value = re.sub(r"\\(?:mathbf|mathrm|text|operatorname)\{([^{}]*)\}", r"\1", value)
    value = re.sub(r"\\frac\{([^{}]*)\}\{([^{}]*)\}", r"(\1)/(\2)", value)
    value = value.replace("^{", "^").replace("_{", "_").replace("}", "")
    value = value.replace("$", "")
    value = value.replace("\\", "")
    return normalize_dashes(value)


def inline_markup(text: str) -> str:
    text = latex_to_text(text)
    tokens: list[str] = []

    def stash(fragment: str) -> str:
        tokens.append(fragment)
        return f"@@TOKEN{len(tokens)-1}@@"

    def link_sub(match: re.Match[str]) -> str:
        label = html.escape(match.group(1))
        url = html.escape(match.group(2), quote=True)
        return stash(f'<link href="{url}" color="#0F766E">{label}</link>')

    text = re.sub(r"\[([^\]]+)\]\((https?://[^)]+)\)", link_sub, text)

    def code_sub(match: re.Match[str]) -> str:
        code = html.escape(match.group(1))
        return stash(f'<font name="DejaVu-Mono" color="#5A2D82">{code}</font>')

    text = re.sub(r"`([^`]+)`", code_sub, text)
    text = html.escape(text)
    text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
    text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", text)
    for idx, fragment in enumerate(tokens):
        text = text.replace(f"@@TOKEN{idx}@@", fragment)
    return text


def styles() -> dict[str, ParagraphStyle]:
    base = getSampleStyleSheet()
    return {
        "body": ParagraphStyle(
            "Body",
            parent=base["BodyText"],
            fontName="DejaVu",
            fontSize=8.5,
            leading=12.0,
            textColor=INK,
            spaceAfter=5,
            alignment=TA_LEFT,
        ),
        "small": ParagraphStyle(
            "Small",
            parent=base["BodyText"],
            fontName="DejaVu",
            fontSize=7,
            leading=9.5,
            textColor=MUTED,
        ),
        "code": ParagraphStyle(
            "Code",
            parent=base["BodyText"],
            fontName="DejaVu-Mono",
            fontSize=7.5,
            leading=10.5,
            textColor=INK,
            backColor=PALE,
            borderColor=RULE,
            borderWidth=0.5,
            borderPadding=5,
            spaceBefore=4,
            spaceAfter=6,
        ),
        "h1": ParagraphStyle(
            "H1",
            parent=base["Heading1"],
            fontName="DejaVu-Bold",
            fontSize=19,
            leading=23,
            textColor=NAVY,
            spaceBefore=12,
            spaceAfter=9,
            keepWithNext=True,
        ),
        "h2": ParagraphStyle(
            "H2",
            parent=base["Heading2"],
            fontName="DejaVu-Bold",
            fontSize=13.5,
            leading=17,
            textColor=colors.HexColor("#0C6D67"),
            spaceBefore=11,
            spaceAfter=6,
            keepWithNext=True,
        ),
        "h3": ParagraphStyle(
            "H3",
            parent=base["Heading3"],
            fontName="DejaVu-Bold",
            fontSize=10.5,
            leading=14,
            textColor=INK,
            spaceBefore=8,
            spaceAfter=4,
            keepWithNext=True,
        ),
        "bullet": ParagraphStyle(
            "Bullet",
            parent=base["BodyText"],
            fontName="DejaVu",
            fontSize=8.4,
            leading=11.8,
            textColor=INK,
            leftIndent=13,
            firstLineIndent=-7,
            bulletIndent=4,
            spaceAfter=2,
        ),
        "number": ParagraphStyle(
            "Number",
            parent=base["BodyText"],
            fontName="DejaVu",
            fontSize=8.4,
            leading=11.8,
            textColor=INK,
            leftIndent=16,
            firstLineIndent=-11,
            spaceAfter=2,
        ),
        "section_label": ParagraphStyle(
            "SectionLabel",
            parent=base["BodyText"],
            fontName="DejaVu-Bold",
            fontSize=8,
            leading=10,
            textColor=TEAL,
            spaceAfter=5,
        ),
    }


def parse_table(lines: list[str], st: dict[str, ParagraphStyle], usable_width: float):
    raw_rows: list[list[str]] = []
    for line in lines:
        cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
        raw_rows.append(cells)
    if len(raw_rows) > 1 and all(re.fullmatch(r":?-{3,}:?", cell or "-") for cell in raw_rows[1]):
        raw_rows.pop(1)
    ncols = max(len(row) for row in raw_rows)
    for row in raw_rows:
        row.extend([""] * (ncols - len(row)))

    if ncols >= 6:
        weights = [0.6, 1.45, 1.05, 1.35, 1.85, 1.85][:ncols]
        font_size = 5.15
        leading = 6.5
    elif ncols == 5:
        weights = [0.75, 1.45, 1.25, 1.45, 1.55]
        font_size = 5.8
        leading = 7.4
    elif ncols == 4:
        weights = [1.2, 1.9, 1.15, 2.0]
        font_size = 6.3
        leading = 8.2
    elif ncols == 3:
        weights = [1.2, 2.0, 2.5]
        font_size = 6.6
        leading = 8.6
    else:
        weights = [1.2] * ncols
        font_size = 7.0
        leading = 9.0
    weights = weights[:ncols]
    scale = usable_width / sum(weights)
    widths = [weight * scale for weight in weights]
    cell_style = ParagraphStyle(
        "TableCell",
        fontName="DejaVu",
        fontSize=font_size,
        leading=leading,
        textColor=INK,
        wordWrap="CJK",
    )
    header_style = ParagraphStyle(
        "TableHeader",
        parent=cell_style,
        fontName="DejaVu-Bold",
        textColor=colors.white,
    )
    data = []
    for r_idx, row in enumerate(raw_rows):
        data.append([
            Paragraph(inline_markup(cell), header_style if r_idx == 0 else cell_style)
            for cell in row
        ])
    table = LongTable(data, colWidths=widths, repeatRows=1, hAlign="LEFT", splitByRow=1)
    table.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), NAVY),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("GRID", (0, 0), (-1, -1), 0.35, RULE),
                ("LEFTPADDING", (0, 0), (-1, -1), 4),
                ("RIGHTPADDING", (0, 0), (-1, -1), 4),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, PALE]),
            ]
        )
    )
    return table


def markdown_flowables(path: Path, st: dict[str, ParagraphStyle], usable_width: float):
    source = normalize_dashes(path.read_text(encoding="utf-8"))
    lines = source.splitlines()
    story = []
    paragraph_parts: list[str] = []
    equation_parts: list[str] = []
    fenced_parts: list[str] = []
    in_equation = False
    in_fence = False

    def flush_paragraph() -> None:
        if paragraph_parts:
            text = " ".join(part.strip() for part in paragraph_parts if part.strip())
            if text:
                story.append(Paragraph(inline_markup(text), st["body"]))
            paragraph_parts.clear()

    def flush_equation() -> None:
        if equation_parts:
            text = " ".join(part.strip() for part in equation_parts if part.strip())
            story.append(Paragraph(inline_markup(text), st["code"]))
            equation_parts.clear()

    def flush_fence() -> None:
        if fenced_parts:
            text = "<br/>".join(html.escape(part) for part in fenced_parts)
            story.append(Paragraph(text, st["code"]))
            fenced_parts.clear()

    i = 0
    while i < len(lines):
        line = lines[i].rstrip()
        stripped = line.strip()
        if stripped.startswith(chr(96) * 3):
            flush_paragraph()
            if in_fence:
                in_fence = False
                flush_fence()
            else:
                in_fence = True
            i += 1
            continue
        if in_fence:
            fenced_parts.append(line)
            i += 1
            continue
        if stripped == "\\[":
            flush_paragraph()
            in_equation = True
            i += 1
            continue
        if stripped == "\\]":
            in_equation = False
            flush_equation()
            i += 1
            continue
        if in_equation:
            equation_parts.append(stripped)
            i += 1
            continue
        if stripped.startswith("|") and stripped.endswith("|"):
            flush_paragraph()
            table_lines = []
            while i < len(lines) and lines[i].strip().startswith("|") and lines[i].strip().endswith("|"):
                table_lines.append(lines[i].strip())
                i += 1
            story.append(parse_table(table_lines, st, usable_width))
            story.append(Spacer(1, 7))
            continue
        if not stripped:
            flush_paragraph()
            i += 1
            continue
        if stripped.startswith("### "):
            flush_paragraph()
            story.append(Paragraph(inline_markup(stripped[4:]), st["h3"]))
        elif stripped.startswith("## "):
            flush_paragraph()
            story.append(Paragraph(inline_markup(stripped[3:]), st["h2"]))
        elif stripped.startswith("# "):
            flush_paragraph()
            story.append(Paragraph(inline_markup(stripped[2:]), st["h1"]))
        elif re.match(r"^-\s+", stripped):
            flush_paragraph()
            content = re.sub(r"^-\s+", "", stripped)
            story.append(Paragraph(inline_markup(content), st["bullet"], bulletText="•"))
        elif re.match(r"^\d+\.\s+", stripped):
            flush_paragraph()
            match = re.match(r"^(\d+)\.\s+(.*)$", stripped)
            assert match
            story.append(Paragraph(f"<b>{match.group(1)}.</b> {inline_markup(match.group(2))}", st["number"]))
        elif stripped.startswith("**") and stripped.endswith("  "):
            flush_paragraph()
            story.append(Paragraph(inline_markup(stripped.rstrip()), st["body"]))
        else:
            paragraph_parts.append(stripped.rstrip("  "))
        i += 1
    flush_paragraph()
    flush_equation()
    flush_fence()
    return story


class DossierDocTemplate(BaseDocTemplate):
    pass


def header_footer(canvas, doc) -> None:
    canvas.saveState()
    width, height = A4
    canvas.setStrokeColor(RULE)
    canvas.setLineWidth(0.4)
    canvas.line(18 * mm, height - 14 * mm, width - 18 * mm, height - 14 * mm)
    canvas.setFont("DejaVu", 7)
    canvas.setFillColor(MUTED)
    canvas.drawString(18 * mm, height - 10.5 * mm, "APEIRON / CONTROLLED PUBLICATION DOSSIER")
    canvas.drawRightString(width - 18 * mm, 10 * mm, f"Page {doc.page}")
    canvas.setFillColor(TEAL)
    canvas.rect(18 * mm, 8 * mm, 14 * mm, 1.2 * mm, fill=1, stroke=0)
    canvas.restoreState()


def cover_page(canvas, doc) -> None:
    canvas.saveState()
    width, height = A4
    canvas.setFillColor(NAVY)
    canvas.rect(0, 0, width, height, fill=1, stroke=0)
    canvas.setStrokeColor(colors.HexColor("#1B6B70"))
    canvas.setLineWidth(0.7)
    for offset in (0, 16, 32, 48):
        canvas.bezier(0, 70 * mm + offset, 55 * mm, 95 * mm + offset, 130 * mm, 42 * mm + offset, width, 74 * mm + offset)
    canvas.restoreState()


def cover_story(st: dict[str, ParagraphStyle]):
    title = ParagraphStyle(
        "CoverTitle",
        fontName="DejaVu-Bold",
        fontSize=27,
        leading=33,
        textColor=colors.white,
        alignment=TA_LEFT,
    )
    kicker = ParagraphStyle(
        "CoverKicker",
        fontName="DejaVu-Bold",
        fontSize=9,
        leading=12,
        textColor=colors.HexColor("#65D6C5"),
        spaceAfter=10,
    )
    subtitle = ParagraphStyle(
        "CoverSubtitle",
        fontName="DejaVu",
        fontSize=12,
        leading=18,
        textColor=colors.HexColor("#D7E7ED"),
    )
    badge = ParagraphStyle(
        "Badge",
        fontName="DejaVu-Bold",
        fontSize=8,
        leading=10,
        textColor=NAVY,
        alignment=TA_CENTER,
    )
    body_white = ParagraphStyle(
        "BodyWhite",
        fontName="DejaVu",
        fontSize=9,
        leading=13,
        textColor=colors.HexColor("#E7F0F3"),
    )
    story = [
        Spacer(1, 17 * mm),
        Paragraph("APEIRON / AME-1 / PUBLICATION MILESTONE", kicker),
        Paragraph("Controlled Publication<br/>Master Dossier", title),
        Spacer(1, 6 * mm),
        Paragraph(
            "Claims register, venue-neutral manuscript, gap audit, canonical operational model specification, and read-only reproducibility guide",
            subtitle,
        ),
        Spacer(1, 10 * mm),
    ]
    badges = Table(
        [
            [Paragraph("v7.13 FROZEN", badge), Paragraph("R1-R3 CLOSED", badge), Paragraph("NO SOLVER RERUN", badge)],
        ],
        colWidths=[48 * mm, 54 * mm, 52 * mm],
        rowHeights=[11 * mm],
    )
    badges.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (0, 0), colors.HexColor("#74D8C9")),
                ("BACKGROUND", (1, 0), (1, 0), colors.HexColor("#F3C969")),
                ("BACKGROUND", (2, 0), (2, 0), colors.HexColor("#AFC9FF")),
                ("BOX", (0, 0), (-1, -1), 0.5, colors.white),
                ("INNERGRID", (0, 0), (-1, -1), 0.5, colors.white),
                ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
            ]
        )
    )
    story.extend(
        [
            badges,
            Spacer(1, 16 * mm),
            Paragraph(
                "Scope statement",
                ParagraphStyle("Scope", parent=kicker, textColor=colors.HexColor("#F3C969")),
            ),
            Paragraph(
                "The terminal v7.13 result is reported only as a numerical gate classification of the frozen homogeneous model. "
                "The package now exposes its operational equations, exact gates, immutable artifacts, and read-only verification route. "
                "This does not claim empirical confirmation, independent numerical reproduction, a fundamental covariant action, or ontological proof. "
                "AP1-AP3 remain a prospective falsification program.",
                body_white,
            ),
            Spacer(1, 12 * mm),
            Paragraph("30 August 2026 / venue-neutral controlled draft", kicker),
            PageBreak(),
        ]
    )
    return story


def manifest_story(st: dict[str, ParagraphStyle], usable_width: float):
    story = [
        Paragraph("Dossier manifest", st["h1"]),
        Paragraph(
            "This milestone PDF is generated from the authoritative publication-latest source files. The numerical state and recovery files were not modified.",
            st["body"],
        ),
    ]
    rows = [["Document", "Role", "SHA-256"]]
    for path, role in [
        (CLAIMS, "German internal claims audit"),
        (MANUSCRIPT, "English scientific master manuscript"),
        (GAPS, "German publication gap report"),
        (MODEL_SPEC, "Canonical operational model specification"),
        (REPRO_README, "Read-only reproducibility guide"),
        (GATE_SPEC, "Machine-readable exact v7.13 gate table"),
        (REPRO_MANIFEST, "Machine-readable SHA-256 manifest"),
        (REPRO_PACKAGE, "Tested reproducibility ZIP"),
    ]:
        rows.append([path.name, role, sha256(path)])
    table_lines = ["|" + "|".join(row) + "|" for row in rows]
    table_lines.insert(1, "|---|---|---|")
    story.extend(
        [
            parse_table(table_lines, st, usable_width),
            Spacer(1, 8),
            KeepTogether(
                [
                    Paragraph("Reading order", st["h2"]),
                    Paragraph(
                        "Part I fixes the publication claim ceiling. Part II is the venue-neutral manuscript. Part III records remaining gaps. Part IV states the canonical operational model. Part V gives the read-only verification instructions.",
                        st["body"],
                    ),
                ]
            ),
            PageBreak(),
        ]
    )
    return story


def document_divider(title: str, subtitle: str, st: dict[str, ParagraphStyle]):
    box = Table(
        [[Paragraph(title, st["h1"])], [Paragraph(subtitle, st["body"])]],
        colWidths=[160 * mm],
    )
    box.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, -1), MINT),
                ("BOX", (0, 0), (-1, -1), 0.8, TEAL),
                ("LEFTPADDING", (0, 0), (-1, -1), 12),
                ("RIGHTPADDING", (0, 0), (-1, -1), 12),
                ("TOPPADDING", (0, 0), (-1, -1), 9),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 9),
            ]
        )
    )
    return [box, Spacer(1, 10)]


def build() -> None:
    register_fonts()
    st = styles()
    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    doc = DossierDocTemplate(
        str(OUTPUT),
        pagesize=A4,
        leftMargin=18 * mm,
        rightMargin=18 * mm,
        topMargin=19 * mm,
        bottomMargin=16 * mm,
        title="Apeiron Controlled Publication Master Dossier",
        author="[AUTHORSHIP TO BE PROVIDED]",
        subject="Claims audit, master manuscript, and publication gap report",
    )
    frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="normal")
    doc.addPageTemplates(
        [
            PageTemplate(id="cover", frames=[frame], onPage=cover_page, autoNextPageTemplate="normal"),
            PageTemplate(id="normal", frames=[frame], onPage=header_footer),
        ]
    )
    story = []
    story.extend(cover_story(st))
    story.extend(manifest_story(st, doc.width))
    story.extend(document_divider("Part I - Controlled claims register", "German internal audit and publication-language ceiling", st))
    story.extend(markdown_flowables(CLAIMS, st, doc.width))
    story.append(PageBreak())
    story.extend(document_divider("Part II - Venue-neutral master manuscript", "English scientific draft with numerical results, limitations, and future falsification program", st))
    story.extend(markdown_flowables(MANUSCRIPT, st, doc.width))
    story.append(PageBreak())
    story.extend(document_divider("Part III - Publication gap report", "German traffic-light audit and controlled resume point", st))
    story.extend(markdown_flowables(GAPS, st, doc.width))
    story.append(PageBreak())
    story.extend(document_divider("Part IV - Canonical operational model specification", "Frozen homogeneous equations, variables, units, parameters, and active-source hashes", st))
    story.extend(markdown_flowables(MODEL_SPEC, st, doc.width))
    story.append(PageBreak())
    story.extend(document_divider("Part V - Read-only reproducibility guide", "Package scope, verification command, and epistemic boundary", st))
    story.extend(markdown_flowables(REPRO_README, st, doc.width))
    doc.build(story)
    print(OUTPUT)


if __name__ == "__main__":
    build()
