from __future__ import annotations

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_PATH = ROOT / "paper-metadata.json"
FONT = ROOT / "assets" / "fonts" / "NotoSansTC-Regular.ttf"
PDF = ROOT / "paper.pdf"
CONTENT = ROOT / "paper-content.json"

INK = colors.HexColor("#2a201a")
BROWN = colors.HexColor("#6f3519")
GREEN = colors.HexColor("#47704f")
PALE = colors.HexColor("#f5eadf")
PALE_GREEN = colors.HexColor("#eaf1e7")
GREY = colors.HexColor("#746c66")
BORDER = colors.HexColor("#d8c9bb")


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}@@"

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


@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(part.strip() for part in paragraph).strip()))
            paragraph.clear()

    while index < len(lines):
        raw = lines[index]
        stripped = raw.strip()
        if not stripped:
            flush()
            index += 1
            continue
        if stripped.startswith("```"):
            flush()
            language = stripped[3:].strip() or "text"
            code: list[str] = []
            index += 1
            while index < len(lines) and not lines[index].strip().startswith("```"):
                code.append(lines[index].rstrip())
                index += 1
            index += 1
            blocks.append(Block("code", {"language": language, "text": "\n".join(code)}))
            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()
            quote_lines: list[str] = []
            while index < len(lines) and lines[index].strip().startswith(">"):
                quote_lines.append(lines[index].strip().lstrip(">").strip())
                index += 1
            blocks.append(Block("callout", " ".join(quote_lines)))
            continue
        if stripped.startswith("|"):
            flush()
            table_lines: list[str] = []
            while index < len(lines) and lines[index].strip().startswith("|"):
                table_lines.append(lines[index].strip())
                index += 1
            rows = [[cell.strip() for cell in line.strip("|").split("|")] for line in table_lines]
            rows = [row for row in rows if not all(re.fullmatch(r":?-{3,}:?", cell.replace(" ", "")) for cell in row)]
            blocks.append(Block("table", rows))
            continue
        if re.match(r"^[-*]\s+", stripped):
            flush()
            items: list[str] = []
            while index < len(lines) and re.match(r"^[-*]\s+", lines[index].strip()):
                items.append(re.sub(r"^[-*]\s+", "", lines[index].strip()))
                index += 1
            blocks.append(Block("list", {"ordered": False, "items": items}))
            continue
        if re.match(r"^\d+\.\s+", stripped):
            flush()
            items = []
            while index < len(lines) and re.match(r"^\d+\.\s+", lines[index].strip()):
                items.append(re.sub(r"^\d+\.\s+", "", lines[index].strip()))
                index += 1
            blocks.append(Block("list", {"ordered": True, "items": items}))
            continue
        paragraph.append(stripped.rstrip("  "))
        index += 1
    flush()
    return blocks


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


def write_citation(meta: dict[str, Any]) -> None:
    author = meta["author"]
    lines = [
        "cff-version: 1.2.0",
        'message: "If you use this research package, please cite the public version below."',
        f"title: {json.dumps(meta['title'], ensure_ascii=False)}",
        "type: report",
        "authors:",
        '  - family-names: "周"',
        '    given-names: "秉漢"',
        f"    alias: {json.dumps(author['nameEnglish'])}",
        f"    orcid: {json.dumps('https://orcid.org/' + author['orcid'])}",
        f"version: {json.dumps(meta['version'])}",
        f"date-released: {json.dumps(meta['datePublished'])}",
        f"doi: {json.dumps(meta['doi'])}",
        f"url: {json.dumps(meta['url'])}",
        'license: "CC-BY-4.0"',
        f"abstract: {json.dumps(meta['description'], ensure_ascii=False)}",
        "keywords:",
        *[f"  - {json.dumps(item, ensure_ascii=False)}" for item in meta.get("keywords", [])],
        "preferred-citation:",
        "  type: report",
        f"  title: {json.dumps(meta['title'], ensure_ascii=False)}",
        "  authors:",
        '    - family-names: "周"',
        '      given-names: "秉漢"',
        f"      alias: {json.dumps(author['nameEnglish'])}",
        f"      orcid: {json.dumps('https://orcid.org/' + author['orcid'])}",
        f"  year: {meta['datePublished'][:4]}",
        f"  version: {json.dumps(meta['version'])}",
        f"  doi: {json.dumps(meta['doi'])}",
        f"  url: {json.dumps(meta['url'])}",
        '  publisher: "上好味研究工作論文系列"',
    ]
    (ROOT / "CITATION.cff").write_text("\n".join(lines) + "\n", encoding="utf-8")


def main() -> None:
    meta = json.loads(META_PATH.read_text(encoding="utf-8"))
    markdown = PAPER.read_text(encoding="utf-8")
    _, sections = split_document(markdown)
    if not sections or not FONT.is_file():
        raise RuntimeError("Paper sections or packaged font are missing")
    for _, blocks in sections:
        for block in blocks:
            if block.kind == "figure":
                path = (ROOT / block.value["path"]).resolve()
                if ROOT.resolve() not in path.parents or not path.is_file():
                    raise FileNotFoundError(path)

    write_citation(meta)
    CONTENT.write_text(json.dumps({
        "schemaVersion": 1,
        "metadata": meta,
        "sections": [
            {"title": title, "blocks": [{"type": block.kind, **({"value": block.value} if block.kind != "figure" else 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.8,
                          leading=14.3, alignment=TA_JUSTIFY, textColor=INK, spaceAfter=6,
                          wordWrap="CJK", allowWidows=0, allowOrphans=0, splitLongWords=True)
    title_style = ParagraphStyle("TitleTC", parent=body, fontSize=18.5, leading=27, alignment=TA_CENTER, spaceAfter=8)
    english_title = ParagraphStyle("EnglishTitle", parent=body, fontSize=9.4, leading=14.2,
                                   alignment=TA_CENTER, textColor=GREY, spaceAfter=6)
    meta_style = ParagraphStyle("Meta", parent=body, fontSize=8.1, leading=12.4, alignment=TA_CENTER, textColor=GREY)
    section_style = ParagraphStyle("Section", parent=body, fontSize=13.4, leading=19.5, textColor=BROWN,
                                   spaceBefore=8, spaceAfter=6, keepWithNext=True)
    subheading_style = ParagraphStyle("Subheading", parent=body, fontSize=10.6, leading=16,
                                      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.4, leading=13.6, textColor=GREEN)
    caption_style = ParagraphStyle("Caption", parent=body, fontSize=7.5, leading=10.8,
                                   alignment=TA_CENTER, textColor=GREY, spaceAfter=6)
    code_style = ParagraphStyle("Code", fontName="Courier", fontSize=6.3, leading=8.1, textColor=INK)
    code_label = ParagraphStyle("CodeLabel", parent=body, fontSize=6.9, leading=9, textColor=BROWN, spaceAfter=2)
    reference_style = ParagraphStyle("Reference", parent=body, fontSize=7.3, leading=10.8,
                                     alignment=TA_LEFT, leftIndent=2 * mm, wordWrap="CJK", splitLongWords=True)

    def header_footer(canvas, doc) -> None:
        canvas.saveState()
        page = canvas.getPageNumber()
        canvas.setFont("NotoTC", 6.5)
        canvas.setFillColor(GREY)
        if page > 1:
            canvas.drawString(16 * mm, A4[1] - 9 * mm, f"{meta['reportNumber']}｜Mavic 3M 光學病害疑似區模型｜版本 v{meta['version']}")
            canvas.setStrokeColor(BORDER)
            canvas.line(16 * mm, A4[1] - 11 * mm, A4[0] - 16 * mm, A4[1] - 11 * mm)
        canvas.drawString(16 * mm, 9 * mm, f"公開工作論文｜未經外部同儕審查｜DOI：{meta['doi']}｜非病原確診或施藥處方")
        canvas.drawRightString(A4[0] - 16 * mm, 9 * mm, str(page))
        canvas.restoreState()

    doc = BaseDocTemplate(
        str(PDF), pagesize=A4, leftMargin=16 * mm, rightMargin=16 * mm, topMargin=17 * mm, bottomMargin=17 * mm,
        title=meta["titleEnglish"], author=meta["author"]["nameEnglish"], subject=meta["description"],
        keywords=", ".join(meta.get("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_blocks = next(blocks for title, blocks in sections if title == "摘要")
    abstract_text = next(str(block.value) for block in abstract_blocks if block.kind == "paragraph")
    cover_summary = abstract_text[:580].rstrip("，。；;,. ") + "……"
    story.extend([
        Spacer(1, 5 * mm),
        Paragraph(f"{meta['reportNumber']}｜公開工作論文｜版本 {meta['version']}", meta_style),
        Spacer(1, 7 * mm),
        Paragraph(inline_markup(meta["title"]), title_style),
        Paragraph(inline_markup(meta["titleEnglish"]), english_title),
        Spacer(1, 4 * mm),
        Paragraph(f"{meta['author']['name']} / {meta['author']['nameEnglish']}", meta_style),
        Paragraph(f"ORCID：{meta['author']['orcid']}｜發布日：{meta['datePublished']}｜DOI：{meta['doi']}", meta_style),
        Spacer(1, 7 * mm),
    ])
    summary_table = Table([[Paragraph("<b>研究摘要</b><br/><br/>" + inline_markup(cover_summary), body)]], colWidths=[155 * mm])
    summary_table.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/>可執行 18 特徵模型、品質與域外拒判、群組切分、模型卡、推論憑證、33 項自動測試、公開資料格式查核與 v1.0 公開發行。"
        "<br/><br/><b>本版本尚未完成</b><br/>臺灣田間飛行、Mavic 3M 真實影像、植物病理真值與跨場域效能驗證；合成測試不等於田間診斷效能。",
        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_table, Spacer(1, 4 * mm), boundary, Spacer(1, 4 * mm),
                  Paragraph(inline_markup(meta["aiUse"]), meta_style), PageBreak()])

    def add_table(rows: list[list[str]]) -> None:
        if len(rows) < 2:
            return
        columns = len(rows[0])
        if any(len(row) != columns for row in rows):
            raise ValueError("Table column counts differ")
        cell = ParagraphStyle("Cell", parent=body, fontSize=6.8 if columns <= 4 else 6.1, leading=10, alignment=TA_LEFT, spaceAfter=0)
        formatted = [[Paragraph(inline_markup(value), cell) for value in row] for row in rows]
        table = LongTable(formatted, 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(166 * mm / width_px, 100 * mm / height_px)
        image = Image(str(path), width=width_px * scale, height=height_px * scale)
        story.append(KeepTogether([image, Spacer(1, 1.3 * mm), Paragraph(inline_markup(value["caption"]), caption_style)]))

    def add_code(value: dict[str, str]) -> None:
        lines = value["text"].splitlines() or [""]
        if max(map(len, lines), default=0) > 110:
            raise ValueError(f"Code block has a PDF-unsafe line: {value['language']}")
        for offset in range(0, len(lines), 31):
            chunk = lines[offset:offset + 31]
            label = Paragraph(f"程式摘錄｜{escape(value['language'])}", code_label)
            pre = Preformatted(escape("\n".join(chunk), quote=False), code_style, maxLineLength=1000)
            box = Table([[pre]], 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(min(150 * mm, len(chunk) * 8.2 + 30)), label, box, Spacer(1, 3 * mm)])

    for section_index, (title, blocks) in enumerate(sections):
        if title == "參考文獻與官方資料":
            story.append(PageBreak())
        elif section_index and re.match(r"^(5|9|12)\.", title):
            story.append(PageBreak())
        else:
            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.append(CondPageBreak(26 * mm))
                story.append(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(PDF), "sha256": sha256(PDF), "sections": len(sections)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
