from __future__ import annotations

import argparse
import hashlib
import json
import re
from dataclasses import dataclass
from html import escape
from pathlib import Path
from typing import Any

from PIL import Image as PillowImage
from reportlab import rl_config
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, 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,
    CondPageBreak,
    Frame,
    Image,
    KeepTogether,
    LongTable,
    PageBreak,
    PageTemplate,
    Paragraph,
    Preformatted,
    Spacer,
    Table,
    TableStyle,
)


ROOT = Path(__file__).resolve().parent
PAPER = ROOT / "paper.md"
META = ROOT / "paper-metadata.json"
FONT = ROOT / "assets" / "fonts" / "NotoSansTC-Regular.ttf"
PDF = ROOT / "paper.pdf"
CONTENT = ROOT / "paper-content.json"

INK = colors.HexColor("#261d18")
BROWN = colors.HexColor("#743c20")
GREEN = colors.HexColor("#456a4d")
GREY = colors.HexColor("#6e6762")
PALE = colors.HexColor("#f6eee5")
PALE_GREEN = colors.HexColor("#eaf2e9")
BORDER = colors.HexColor("#d7c8ba")


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


def inline_markup(value: str) -> str:
    tokens: list[tuple[str, str, str]] = []

    def hold(kind: str, label: str, target: str = "") -> str:
        tokens.append((kind, label, target))
        return f"@@TOKEN{len(tokens) - 1}@@"

    text = value.strip()
    text = re.sub(r"\[([^]]+)\]\((https?://[^)]+)\)", lambda m: hold("link", m.group(1), m.group(2)), text)
    text = re.sub(r"`([^`]+)`", lambda m: hold("code", m.group(1)), text)
    text = escape(text)
    text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
    text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", text)
    for index, (kind, label, target) in enumerate(tokens):
        replacement = (
            f"<link href='{escape(target, quote=True)}' color='#743c20'>{escape(label)}</link>"
            if kind == "link"
            else f"<font name='NotoTC' color='#743c20'>{escape(label)}</font>"
        )
        text = text.replace(f"@@TOKEN{index}@@", replacement)
    return text


@dataclass
class Block:
    kind: str
    value: Any


def parse_blocks(lines: list[str]) -> list[Block]:
    blocks: list[Block] = []
    paragraph: list[str] = []
    index = 0

    def flush() -> None:
        if paragraph:
            blocks.append(Block("paragraph", " ".join(value.strip() for value in paragraph).strip()))
            paragraph.clear()

    while index < len(lines):
        stripped = lines[index].strip()
        if not stripped:
            flush()
            index += 1
            continue
        if stripped.startswith("```"):
            flush()
            language = stripped[3:].strip() or "text"
            values: list[str] = []
            index += 1
            while index < len(lines) and not lines[index].strip().startswith("```"):
                values.append(lines[index].rstrip())
                index += 1
            index += 1
            blocks.append(Block("code", {"language": language, "text": "\n".join(values)}))
            continue
        image = re.fullmatch(r"!\[([^]]+)\]\(([^)]+)\)", stripped)
        if image:
            flush()
            blocks.append(Block("figure", {"caption": image.group(1), "path": image.group(2)}))
            index += 1
            continue
        if stripped.startswith("### "):
            flush()
            blocks.append(Block("subheading", stripped[4:]))
            index += 1
            continue
        if stripped.startswith(">"):
            flush()
            values = []
            while index < len(lines) and lines[index].strip().startswith(">"):
                values.append(lines[index].strip().lstrip(">").strip())
                index += 1
            blocks.append(Block("callout", " ".join(values)))
            continue
        if stripped.startswith("|"):
            flush()
            values = []
            while index < len(lines) and lines[index].strip().startswith("|"):
                values.append([cell.strip() for cell in lines[index].strip().strip("|").split("|")])
                index += 1
            values = [row for row in values if not all(re.fullmatch(r":?-{3,}:?", cell.replace(" ", "")) for cell in row)]
            blocks.append(Block("table", values))
            continue
        if re.match(r"^[-*]\s+", stripped):
            flush()
            values = []
            while index < len(lines) and re.match(r"^[-*]\s+", lines[index].strip()):
                values.append(re.sub(r"^[-*]\s+", "", lines[index].strip()))
                index += 1
            blocks.append(Block("list", {"ordered": False, "items": values}))
            continue
        if re.match(r"^\d+\.\s+", stripped):
            flush()
            values = []
            while index < len(lines) and re.match(r"^\d+\.\s+", lines[index].strip()):
                values.append(re.sub(r"^\d+\.\s+", "", lines[index].strip()))
                index += 1
            blocks.append(Block("list", {"ordered": True, "items": values}))
            continue
        paragraph.append(stripped)
        index += 1
    flush()
    return blocks


def split_sections(markdown: str) -> list[tuple[str, list[Block]]]:
    sections: list[tuple[str, list[Block]]] = []
    title: str | None = None
    lines: list[str] = []
    for line in markdown.replace("\r\n", "\n").splitlines():
        if line.startswith("## "):
            if title is not None:
                sections.append((title, parse_blocks(lines)))
            title = line[3:].strip()
            lines = []
        elif title is not None:
            lines.append(line)
    if title is not None:
        sections.append((title, parse_blocks(lines)))
    return sections


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--paper", default=str(PAPER))
    parser.add_argument("--metadata", default=str(META))
    parser.add_argument("--content", default=str(CONTENT))
    parser.add_argument("--output", default=str(PDF))
    parser.add_argument("--allow-draft", action="store_true")
    args = parser.parse_args()
    paper_path = Path(args.paper).resolve()
    metadata_path = Path(args.metadata).resolve()
    content_path = Path(args.content).resolve()
    output_path = Path(args.output).resolve()
    metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
    version = str(metadata["version"])
    doi = str(metadata.get("doi") or "")
    draft = bool(args.allow_draft)
    public_status = str(metadata.get("publicationStatus", ""))
    if not draft and (
        not re.fullmatch(r"\d+\.\d+(?:\.\d+)?", version)
        or not re.fullmatch(r"10\.5281/zenodo\.\d+", doi)
        or public_status != "公開研究工作論文"
    ):
        raise RuntimeError("public PDF requires a semantic version, an approved Zenodo DOI, and public working-paper status")
    if draft and "草稿" not in str(metadata.get("publicationStatus", "")):
        raise RuntimeError("--allow-draft requires an explicit draft publicationStatus")
    markdown = paper_path.read_text(encoding="utf-8")
    sections = split_sections(markdown)
    if not sections or not FONT.is_file():
        raise RuntimeError("paper sections or font are missing")
    for _, blocks in sections:
        for block in blocks:
            if block.kind == "figure" and not (ROOT / block.value["path"]).is_file():
                raise FileNotFoundError(ROOT / block.value["path"])

    content_path.write_text(json.dumps({
        "schemaVersion": 1,
        "metadata": metadata,
        "sections": [
            {"title": title, "blocks": [{"type": block.kind, "value": block.value} for block in blocks]}
            for title, blocks in sections
        ],
    }, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    rl_config.invariant = 1
    pdfmetrics.registerFont(TTFont("NotoTC", str(FONT)))
    styles = getSampleStyleSheet()
    body = ParagraphStyle(
        "BodyTC", parent=styles["BodyText"], fontName="NotoTC", fontSize=8.6, leading=14.0,
        textColor=INK, alignment=TA_JUSTIFY, wordWrap="CJK", allowWidows=0, allowOrphans=0,
        splitLongWords=True, spaceAfter=5.5,
    )
    title_style = ParagraphStyle("TitleTC", parent=body, fontSize=18.2, leading=26.5, alignment=TA_CENTER, spaceAfter=8)
    english_style = ParagraphStyle("English", parent=body, fontSize=9.0, leading=13.5, alignment=TA_CENTER, textColor=GREY)
    meta_style = ParagraphStyle("Meta", parent=body, fontSize=8.0, leading=12, alignment=TA_CENTER, textColor=GREY)
    section_style = ParagraphStyle("Section", parent=body, fontSize=13.2, leading=19, textColor=BROWN, spaceBefore=8, spaceAfter=6, keepWithNext=True)
    subheading_style = ParagraphStyle("Subheading", parent=body, fontSize=10.4, leading=15.5, textColor=INK, spaceBefore=5, spaceAfter=4, keepWithNext=True)
    list_style = ParagraphStyle("List", parent=body, leftIndent=7 * mm, firstLineIndent=-4 * mm, spaceAfter=3)
    callout_style = ParagraphStyle("Callout", parent=body, fontSize=8.3, leading=13.5, textColor=GREEN)
    caption_style = ParagraphStyle("Caption", parent=body, fontSize=7.4, leading=10.6, alignment=TA_CENTER, textColor=GREY)
    reference_style = ParagraphStyle("Reference", parent=body, fontSize=7.1, leading=10.4, alignment=TA_LEFT, splitLongWords=True)
    code_style = ParagraphStyle("Code", fontName="Courier", fontSize=6.2, leading=8.0, textColor=INK)

    def header_footer(canvas, doc) -> None:
        canvas.saveState()
        page = canvas.getPageNumber()
        canvas.setFont("NotoTC", 6.5)
        canvas.setFillColor(GREY)
        if page > 1:
            running_title = metadata.get("runningTitle", "Mavic 3M 多光譜資料鏈工程研究")
            canvas.drawString(16 * mm, A4[1] - 9 * mm, f"SHWRP-2026-034 · {running_title} · v{version}")
            canvas.setStrokeColor(BORDER)
            canvas.line(16 * mm, A4[1] - 11 * mm, A4[0] - 16 * mm, A4[1] - 11 * mm)
        identifier = f"DOI {doi}" if doi else "修訂草稿 · 新版本 DOI 尚未建立"
        canvas.drawString(16 * mm, 9 * mm, f"{metadata['publicationStatus']} · 未經外部同儕審查 · 非實飛／診斷證據 · {identifier}")
        canvas.drawRightString(A4[0] - 16 * mm, 9 * mm, str(page))
        canvas.restoreState()

    doc = BaseDocTemplate(
        str(output_path), pagesize=A4, leftMargin=16 * mm, rightMargin=16 * mm, topMargin=17 * mm, bottomMargin=17 * mm,
        title=metadata["titleEnglish"], author=metadata["author"]["nameEnglish"],
        subject=f'{metadata["description"]}{f" DOI: {doi}" if doi else ""}', keywords=", ".join(metadata["keywords"]),
    )
    frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="normal")
    doc.addPageTemplates([PageTemplate(id="paper", frames=[frame], onPage=header_footer)])
    story: list[Any] = []

    abstract = next(blocks for title, blocks in sections if title == "摘要")
    abstract_text = next(str(block.value) for block in abstract if block.kind == "paragraph")
    cover_summary = abstract_text[:620].rstrip("。；，,. ") + "。"
    story.extend([
        Spacer(1, 5 * mm),
        Paragraph(f"SHWRP-2026-034 · {metadata['publicationStatus']} · 版本 {version}", meta_style),
        Spacer(1, 7 * mm),
        Paragraph(inline_markup(metadata["title"]), title_style),
        Paragraph(inline_markup(metadata["titleEnglish"]), english_style),
        Spacer(1, 5 * mm),
        Paragraph("周秉漢 / CHOU, BING-HAN", meta_style),
        Paragraph(
            f"ORCID 0009-0000-7018-5096 · {metadata['dateModified']} · "
            + (f"DOI {doi}" if doi else f"既有 v1.0 DOI {metadata.get('previousVersionDoi', '未提供')}; 本草稿未發布"),
            meta_style,
        ),
        Spacer(1, 7 * mm),
    ])
    summary = Table([[Paragraph("<b>研究摘要</b><br/><br/>" + inline_markup(cover_summary), body)]], colWidths=[155 * mm])
    summary.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), PALE), ("BOX", (0, 0), (-1, -1), 0.7, BORDER),
        ("LEFTPADDING", (0, 0), (-1, -1), 7 * mm), ("RIGHTPADDING", (0, 0), (-1, -1), 7 * mm),
        ("TOPPADDING", (0, 0), (-1, -1), 5 * mm), ("BOTTOMPADDING", (0, 0), (-1, -1), 5 * mm),
    ]))
    boundary = Table([[Paragraph(
        "<b>證據邊界</b><br/>" + inline_markup(metadata.get(
            "evidenceBoundary",
            "本版本只完成規格約束的 deterministic digital twin。沒有起飛、沒有真實 Mavic 3M 影像、沒有真實農地與地面真值；合成 precision、recall 與 IoU 不能當成田間準確率。",
        )),
        callout_style,
    )]], colWidths=[155 * mm])
    boundary.setStyle(TableStyle([
        ("BACKGROUND", (0, 0), (-1, -1), PALE_GREEN), ("BOX", (0, 0), (-1, -1), 0.7, colors.HexColor("#b5c6b0")),
        ("LEFTPADDING", (0, 0), (-1, -1), 6 * mm), ("RIGHTPADDING", (0, 0), (-1, -1), 6 * mm),
        ("TOPPADDING", (0, 0), (-1, -1), 4 * mm), ("BOTTOMPADDING", (0, 0), (-1, -1), 4 * mm),
    ]))
    story.extend([summary, Spacer(1, 4 * mm), boundary, Spacer(1, 5 * mm), Paragraph(inline_markup(metadata["aiUse"]), meta_style), PageBreak()])

    def add_table(rows: list[list[str]]) -> None:
        if len(rows) < 2:
            return
        columns = len(rows[0])
        if columns > 5 or any(len(row) != columns for row in rows):
            raise ValueError("PDF table has unsupported or inconsistent columns")
        cell_style = ParagraphStyle("Cell", parent=body, fontSize=6.7 if columns <= 4 else 6.0, leading=9.8, alignment=TA_LEFT, spaceAfter=0)
        cells = [[Paragraph(inline_markup(value), cell_style) for value in row] for row in rows]
        table = LongTable(cells, colWidths=[doc.width / columns] * columns, repeatRows=1, splitByRow=1)
        table.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, 0), PALE), ("TEXTCOLOR", (0, 0), (-1, 0), BROWN),
            ("GRID", (0, 0), (-1, -1), 0.35, BORDER), ("VALIGN", (0, 0), (-1, -1), "TOP"),
            ("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3),
            ("TOPPADDING", (0, 0), (-1, -1), 3.5), ("BOTTOMPADDING", (0, 0), (-1, -1), 3.5),
        ]))
        story.extend([table, Spacer(1, 3 * mm)])

    def add_figure(value: dict[str, str]) -> None:
        path = (ROOT / value["path"]).resolve()
        with PillowImage.open(path) as bitmap:
            width_px, height_px = bitmap.size
        scale = min(165 * mm / width_px, 98 * mm / height_px)
        image = Image(str(path), width=width_px * scale, height=height_px * scale)
        story.append(KeepTogether([image, Spacer(1, 1.2 * mm), Paragraph(inline_markup(value["caption"]), caption_style), Spacer(1, 2 * mm)]))

    def add_code(value: dict[str, str]) -> None:
        lines = value["text"].splitlines() or [""]
        if max(map(len, lines), default=0) > 105:
            raise ValueError("code block contains a PDF-unsafe line")
        for offset in range(0, len(lines), 30):
            code = Preformatted(escape("\n".join(lines[offset:offset + 30]), quote=False), code_style)
            box = Table([[code]], colWidths=[doc.width - 8 * mm])
            box.setStyle(TableStyle([
                ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f1eee9")), ("BOX", (0, 0), (-1, -1), 0.5, BORDER),
                ("LEFTPADDING", (0, 0), (-1, -1), 4 * mm), ("RIGHTPADDING", (0, 0), (-1, -1), 4 * mm),
                ("TOPPADDING", (0, 0), (-1, -1), 3 * mm), ("BOTTOMPADDING", (0, 0), (-1, -1), 3 * mm),
            ]))
            story.extend([CondPageBreak(45 * mm), box, Spacer(1, 3 * mm)])

    for section_index, (title, blocks) in enumerate(sections):
        if section_index:
            story.append(CondPageBreak(34 * mm))
        story.append(Paragraph(inline_markup(title), section_style))
        reference_mode = title == "參考文獻與資料來源"
        for block in blocks:
            if block.kind == "paragraph":
                story.append(Paragraph(inline_markup(str(block.value)), reference_style if reference_mode else body))
            elif block.kind == "subheading":
                story.extend([CondPageBreak(25 * mm), Paragraph(inline_markup(str(block.value)), subheading_style)])
            elif block.kind == "callout":
                table = Table([[Paragraph(inline_markup(str(block.value)), callout_style)]], colWidths=[doc.width])
                table.setStyle(TableStyle([
                    ("BACKGROUND", (0, 0), (-1, -1), PALE_GREEN), ("LINEBEFORE", (0, 0), (0, -1), 2.2, GREEN),
                    ("LEFTPADDING", (0, 0), (-1, -1), 4 * mm), ("RIGHTPADDING", (0, 0), (-1, -1), 4 * mm),
                    ("TOPPADDING", (0, 0), (-1, -1), 3 * mm), ("BOTTOMPADDING", (0, 0), (-1, -1), 3 * mm),
                ]))
                story.extend([table, Spacer(1, 2 * mm)])
            elif block.kind == "list":
                for item_index, item in enumerate(block.value["items"], 1):
                    marker = f"{item_index}." if block.value["ordered"] else "•"
                    story.append(Paragraph(f"{marker}&nbsp;&nbsp;{inline_markup(item)}", reference_style if reference_mode else list_style))
            elif block.kind == "table":
                add_table(block.value)
            elif block.kind == "figure":
                add_figure(block.value)
            elif block.kind == "code":
                add_code(block.value)
            else:
                raise ValueError(block.kind)

    doc.build(story)
    print(json.dumps({"status": "PASSED", "pdf": str(output_path), "pagesPendingRender": True, "sha256": sha256(output_path)}, ensure_ascii=False))


if __name__ == "__main__":
    main()

