from __future__ import annotations

import json
import re
from html import escape
from pathlib import Path

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,
    ListFlowable,
    ListItem,
    LongTable,
    PageBreak,
    PageTemplate,
    Paragraph,
    Spacer,
    Table,
    TableStyle,
)


ROOT = Path(__file__).resolve().parents[2]
DIR = Path(__file__).resolve().parent
META_PATH = DIR / "paper-metadata.json"
MARKDOWN_PATH = DIR / "paper.md"
RESULTS_PATH = DIR / "analysis-results.json"
FONT_PATH = ROOT / "public" / "fonts" / "NotoSansTC-Regular.ttf"
PDF_PATH = DIR / "paper.pdf"
CONTENT_PATH = DIR / "paper-content.json"

META = json.loads(META_PATH.read_text(encoding="utf-8"))
RESULTS = json.loads(RESULTS_PATH.read_text(encoding="utf-8"))
MARKDOWN = MARKDOWN_PATH.read_text(encoding="utf-8").replace("\r\n", "\n")


def ascii_dashes(text: str) -> str:
    """Keep the PDF free of troublesome non-ASCII dash glyphs."""
    return (
        text.replace("\u2010", "-")
        .replace("\u2011", "-")
        .replace("\u2012", "-")
        .replace("\u2013", "-")
        .replace("\u2014", "-")
        .replace("\u2015", "-")
        .replace("\u2212", "-")
    )


def clean_inline(text: str) -> str:
    value = ascii_dashes(text)
    value = re.sub(r"\[([^\]]+)\]\((https?://[^)]+)\)", r"\1", value)
    value = re.sub(r"\*\*(.+?)\*\*", r"\1", value)
    value = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", value)
    value = re.sub(r"`([^`]+)`", r"\1", value)
    value = re.sub(r"<br\s*/?>", " ", value, flags=re.IGNORECASE)
    return re.sub(r"\s+", " ", value).strip()


def paragraph_html(text: str, preserve_lines: bool = False) -> str:
    value = ascii_dashes(text)
    value = re.sub(r"<br\s*/?>", "\n", value, flags=re.IGNORECASE)
    value = escape(value)
    value = re.sub(
        r"\[([^\]]+)\]\((https?://[^)]+)\)",
        r'<a href="\2">\1</a>',
        value,
    )
    value = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", value)
    value = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<i>\1</i>", value)
    value = re.sub(r"`([^`]+)`", r"<font name='NotoTC'>\1</font>", value)
    if preserve_lines:
        value = value.replace("\n", "<br/>")
    return value


def split_sections(markdown: str) -> list[tuple[str, list[str]]]:
    sections: list[tuple[str, list[str]]] = []
    current = "front"
    buffer: list[str] = []
    for raw in markdown.splitlines():
        if raw.startswith("## "):
            if current != "front":
                sections.append((current, buffer))
            current = clean_inline(raw[3:])
            buffer = []
        else:
            buffer.append(raw)
    if current != "front":
        sections.append((current, buffer))
    return sections


SECTIONS = split_sections(MARKDOWN)
SECTION_MAP = {title: lines for title, lines in SECTIONS}


def find_section(*names: str) -> list[str]:
    for name in names:
        if name in SECTION_MAP:
            return SECTION_MAP[name]
    return []


def markdown_paragraphs(lines: list[str]) -> list[str]:
    output: list[str] = []
    buffer: list[str] = []
    in_fence = False
    for raw in lines:
        line = raw.strip()
        if line.startswith("```"):
            in_fence = not in_fence
            if buffer:
                output.append(" ".join(buffer))
                buffer = []
            continue
        if in_fence:
            continue
        is_structural = (
            line.startswith(("### ", "|", "![", "- ", ">"))
            or re.match(r"^\d+\.\s", line) is not None
        )
        if not line or is_structural:
            if buffer:
                output.append(" ".join(buffer))
                buffer = []
            continue
        buffer.append(line)
    if buffer:
        output.append(" ".join(buffer))
    return output


def parse_table(lines: list[str]) -> tuple[list[list[str]], int]:
    table_lines: list[str] = []
    consumed = 0
    for raw in lines:
        if not raw.strip().startswith("|"):
            break
        table_lines.append(raw.strip())
        consumed += 1
    rows = [
        [clean_inline(cell) for cell in row.strip("|").split("|")]
        for row in table_lines
    ]
    if len(rows) < 2:
        return rows, consumed
    return [rows[0], *rows[2:]], consumed


def parse_blocks(lines: list[str]) -> list[dict]:
    """Produce only block types already supported by the research page."""
    blocks: list[dict] = []
    paragraph_buffer: list[str] = []
    i = 0

    def flush() -> None:
        nonlocal paragraph_buffer
        if paragraph_buffer:
            text = clean_inline(" ".join(paragraph_buffer))
            if text:
                blocks.append({"type": "paragraph", "text": text})
            paragraph_buffer = []

    while i < len(lines):
        line = lines[i].strip()
        if not line:
            flush()
            i += 1
            continue
        if line.startswith("### "):
            flush()
            blocks.append({"type": "heading", "text": clean_inline(line[4:])})
            i += 1
            continue
        image = re.match(r"!\[([^\]]+)\]\(([^)]+)\)", line)
        if image:
            flush()
            blocks.append(
                {
                    "type": "figure",
                    "caption": clean_inline(image.group(1)),
                    "filename": Path(image.group(2)).name,
                }
            )
            i += 1
            continue
        if line.startswith("|") and i + 1 < len(lines) and "---" in lines[i + 1]:
            flush()
            rows, consumed = parse_table(lines[i:])
            if rows:
                blocks.append(
                    {"type": "table", "headers": rows[0], "rows": rows[1:]}
                )
            i += consumed
            continue
        if line.startswith("```"):
            flush()
            i += 1
            code_lines: list[str] = []
            while i < len(lines) and not lines[i].strip().startswith("```"):
                code_lines.append(lines[i].rstrip())
                i += 1
            i += 1 if i < len(lines) else 0
            text = clean_inline("；".join(part for part in code_lines if part.strip()))
            if text:
                blocks.append({"type": "paragraph", "text": text})
            continue
        if line.startswith(">"):
            flush()
            quote_lines: list[str] = []
            while i < len(lines) and lines[i].strip().startswith(">"):
                quote_lines.append(lines[i].strip().lstrip(">").strip())
                i += 1
            blocks.append(
                {
                    "type": "paragraph",
                    "text": clean_inline(" ".join(quote_lines)),
                }
            )
            continue
        ordered = re.match(r"^\d+\.\s+(.+)", line)
        if ordered:
            flush()
            items: list[str] = []
            while i < len(lines):
                match = re.match(r"^\d+\.\s+(.+)", lines[i].strip())
                if not match:
                    break
                items.append(clean_inline(match.group(1)))
                i += 1
            blocks.append({"type": "list", "ordered": True, "items": items})
            continue
        if line.startswith("- "):
            flush()
            items: list[str] = []
            while i < len(lines) and lines[i].strip().startswith("- "):
                items.append(clean_inline(lines[i].strip()[2:]))
                i += 1
            blocks.append({"type": "list", "ordered": False, "items": items})
            continue
        paragraph_buffer.append(line)
        i += 1
    flush()
    return blocks


def parse_keywords(lines: list[str]) -> list[str]:
    for raw in lines:
        line = clean_inline(raw)
        match = re.match(r"^(?:關鍵詞|關鍵字|Keywords)\s*[:：]\s*(.+)$", line, re.I)
        if match:
            return [
                item.strip()
                for item in re.split(r"[、，,;；]", match.group(1))
                if item.strip()
            ]
    return []


def parse_references() -> list[list[str]]:
    reference_lines: list[str] = []
    for title, lines in SECTIONS:
        if title.startswith("參考文獻") or title.startswith("資料來源"):
            reference_lines.extend(lines)

    references: list[list[str]] = []
    for raw in reference_lines:
        line = raw.strip()
        match = re.match(r"^(?:\d+\.|-)\s+(.+)", line)
        if not match:
            continue
        raw_label = match.group(1)
        url_match = re.search(r"\((https?://[^)]+)\)", raw_label)
        if not url_match:
            url_match = re.search(r"(https?://[^\s)>]+)", raw_label)
        url = ascii_dashes(url_match.group(1).rstrip(".,;；。")) if url_match else ""
        label = clean_inline(raw_label)
        references.append([label, url])
    return references


abstract_zh_lines = find_section("摘要")
abstract_en_lines = find_section("Abstract", "英文摘要")
abstract_zh_parts = [
    clean_inline(part)
    for part in markdown_paragraphs(abstract_zh_lines)
    if not clean_inline(part).startswith(("關鍵詞", "關鍵字"))
]
abstract_en_parts = [
    clean_inline(part)
    for part in markdown_paragraphs(abstract_en_lines)
    if not clean_inline(part).lower().startswith("keywords")
]

if not abstract_zh_parts:
    raise ValueError("paper.md must contain a non-empty '## 摘要' section")
if not abstract_en_parts:
    raise ValueError("paper.md must contain a non-empty '## Abstract' section")

references = parse_references()
web_sections: list[dict] = []
for title, lines in SECTIONS:
    if re.match(r"^\d+\.\s", title):
        web_sections.append(
            {
                "id": f"section-{len(web_sections) + 1}",
                "title": ascii_dashes(title),
                "blocks": parse_blocks(lines),
            }
        )

observed = RESULTS["rule_observed_counts"]
content = {
    "reportNumber": META["reportNumber"],
    "version": META["version"],
    "date": META["datePublished"],
    "revisionDate": META["dateModified"],
    "revisionNote": META.get(
        "revisionNote",
        "版本1.0：完成60案例公開規則內容分析、配送成本與定價政策合成情境、"
        "六張原創圖表、法律與授權邊界及公開重現資料包。",
    ),
    "doi": META["doi"],
    "zenodoUrl": META["zenodoUrl"],
    "status": META.get("status", "公開工作論文・未經外部同儕審查"),
    "license": META["license"],
    "licenseUrl": META["licenseUrl"],
    "titleZh": ascii_dashes(META["title"]),
    "titleEn": ascii_dashes(META["titleEnglish"]),
    "author": META["author"]["name"],
    "authorEn": META["author"]["nameEnglish"],
    "orcid": META["author"]["orcid"],
    "email": META["author"]["email"],
    "abstractZh": "\n\n".join(abstract_zh_parts),
    "abstractEn": "\n\n".join(abstract_en_parts),
    "keywords": parse_keywords(abstract_zh_lines),
    "highlights": [
        [str(RESULTS["case_count"]), "公開規則案例"],
        [str(observed["minimum_order_observed"]), "訂購數量／金額條件可觀察"],
        [str(RESULTS["policy_scenario_count"]), "合成政策情境"],
        [str(RESULTS["figures"]), "原創圖表"],
    ],
    "sections": web_sections,
    "references": references,
}
CONTENT_PATH.write_text(
    json.dumps(content, ensure_ascii=False, indent=2),
    encoding="utf-8",
)


if not FONT_PATH.exists():
    raise FileNotFoundError(f"Noto Sans TC font not found: {FONT_PATH}")
pdfmetrics.registerFont(TTFont("NotoTC", str(FONT_PATH)))

styles = getSampleStyleSheet()
body = ParagraphStyle(
    "BodyTC",
    fontName="NotoTC",
    fontSize=9.0,
    leading=15.0,
    textColor=colors.HexColor("#241c17"),
    alignment=TA_JUSTIFY,
    spaceAfter=7,
    allowWidows=0,
    allowOrphans=0,
)
title_style = ParagraphStyle(
    "TitleTC",
    parent=body,
    fontSize=19,
    leading=29,
    alignment=TA_CENTER,
    spaceAfter=12,
)
section_style = ParagraphStyle(
    "H2TC",
    parent=body,
    fontSize=14.5,
    leading=21,
    textColor=colors.HexColor("#6f3519"),
    spaceBefore=11,
    spaceAfter=8,
    keepWithNext=True,
)
subsection_style = ParagraphStyle(
    "H3TC",
    parent=body,
    fontSize=11.2,
    leading=17,
    textColor=colors.HexColor("#35261e"),
    spaceBefore=8,
    spaceAfter=5,
    keepWithNext=True,
)
meta_style = ParagraphStyle(
    "MetaTC",
    parent=body,
    fontSize=8.2,
    leading=13,
    alignment=TA_CENTER,
    textColor=colors.HexColor("#625b55"),
)
caption_style = ParagraphStyle(
    "CaptionTC",
    parent=body,
    fontSize=7.5,
    leading=10.5,
    alignment=TA_CENTER,
    textColor=colors.HexColor("#625b55"),
    spaceAfter=8,
)
reference_style = ParagraphStyle(
    "ReferenceTC",
    parent=body,
    fontSize=7.2,
    leading=10,
    alignment=TA_LEFT,
    spaceAfter=1.5,
)
formula_style = ParagraphStyle(
    "FormulaTC",
    parent=body,
    fontSize=8.3,
    leading=13.2,
    alignment=TA_LEFT,
    textColor=colors.HexColor("#3c3029"),
    leftIndent=4,
    rightIndent=4,
)
cover_box_style = ParagraphStyle(
    "CoverBox",
    parent=body,
    fontSize=9.6,
    leading=16.5,
    alignment=TA_CENTER,
)


def draw_header_footer(canvas, doc) -> None:
    canvas.saveState()
    page_number = canvas.getPageNumber()
    canvas.setFont("NotoTC", 6.8)
    canvas.setFillColor(colors.HexColor("#857b73"))
    if page_number > 1:
        header = f"{META['reportNumber']}｜{ascii_dashes(META['title'])}"
        if len(header) > 55:
            header = header[:54] + "…"
        canvas.drawString(16 * mm, A4[1] - 9 * mm, header)
        canvas.setStrokeColor(colors.HexColor("#e4d8cc"))
        canvas.setLineWidth(0.35)
        canvas.line(16 * mm, A4[1] - 11 * mm, A4[0] - 16 * mm, A4[1] - 11 * mm)
    footer = (
        f"{META['reportNumber']}｜公開工作論文 v{META['version']}｜"
        "CC BY 4.0（限作者有權授權內容）"
    )
    canvas.drawString(16 * mm, 9 * mm, footer)
    canvas.drawRightString(A4[0] - 16 * mm, 9 * mm, str(page_number))
    canvas.restoreState()


doc = BaseDocTemplate(
    str(PDF_PATH),
    pagesize=A4,
    leftMargin=16 * mm,
    rightMargin=16 * mm,
    topMargin=17 * mm,
    bottomMargin=17 * mm,
    title=ascii_dashes(META["title"]),
    author=META["author"]["name"],
    subject="臺灣便當業者自架訂餐與外送系統之外送費定價情境分析",
    keywords=", ".join(content["keywords"]),
)
frame = Frame(
    doc.leftMargin,
    doc.bottomMargin,
    doc.width,
    doc.height,
    id="normal",
)
doc.addPageTemplates(
    [PageTemplate(id="paper", frames=[frame], onPage=draw_header_footer)]
)
story: list = []

story.extend(
    [
        Spacer(1, 10 * mm),
        Paragraph(
            f"{META['reportNumber']}｜公開工作論文｜版本 {META['version']}",
            meta_style,
        ),
        Spacer(1, 11 * mm),
        Paragraph(escape(ascii_dashes(META["title"])), title_style),
        Paragraph(escape(ascii_dashes(META["titleEnglish"])), meta_style),
        Spacer(1, 10 * mm),
        Paragraph(
            f"{META['author']['name']} / {META['author']['nameEnglish']}",
            meta_style,
        ),
        Spacer(1, 3 * mm),
        Paragraph(
            f"ORCID：{META['author']['orcid']}｜{META['author']['email']}<br/>"
            f"公開日期：{META['datePublished']}｜未經外部同儕審查<br/>"
            f"DOI：<a href='https://doi.org/{META['doi']}'>{META['doi']}</a><br/>"
            "授權：CC BY 4.0（僅適用於作者有權授權的原創分析、文字、"
            "研究程式與原創圖表；第三方內容仍依原權利條款）",
            meta_style,
        ),
        Spacer(1, 10 * mm),
    ]
)

summary = abstract_zh_parts[0]
if len(summary) > 380:
    summary = summary[:379].rstrip("，、；： ") + "……"
cover_box = Table(
    [
        [
            Paragraph(
                "<b>研究摘要</b><br/><br/>" + paragraph_html(summary),
                cover_box_style,
            )
        ]
    ],
    colWidths=[155 * mm],
)
cover_box.setStyle(
    TableStyle(
        [
            ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f5eadf")),
            ("BOX", (0, 0), (-1, -1), 0.7, colors.HexColor("#d9cbbd")),
            ("LEFTPADDING", (0, 0), (-1, -1), 9 * mm),
            ("RIGHTPADDING", (0, 0), (-1, -1), 9 * mm),
            ("TOPPADDING", (0, 0), (-1, -1), 6 * mm),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 6 * mm),
        ]
    )
)
rights_box = Table(
    [
        [
            Paragraph(
                "<b>證據與定價邊界</b><br/>"
                "60筆案例是目的性選取的公開規則樣本；未觀察到不等於業者未提供。"
                "成本、費率、門檻與會員情境均為研究者設定的敏感度分析，"
                "不是市場平均、正式報價、法律意見或同業共同定價建議。"
                "自架訂餐系統也不等於自有配送車隊。",
                formula_style,
            )
        ]
    ],
    colWidths=[155 * mm],
)
rights_box.setStyle(
    TableStyle(
        [
            ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f2f6f0")),
            ("BOX", (0, 0), (-1, -1), 0.6, colors.HexColor("#b8c9b5")),
            ("LEFTPADDING", (0, 0), (-1, -1), 7 * mm),
            ("RIGHTPADDING", (0, 0), (-1, -1), 7 * mm),
            ("TOPPADDING", (0, 0), (-1, -1), 4 * mm),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 4 * mm),
        ]
    )
)
story.extend(
    [
        cover_box,
        Spacer(1, 6 * mm),
        rights_box,
        Spacer(1, 6 * mm),
        Paragraph(
            "研究定位：公開、可搜尋、可重現、可更正的工作論文；"
            "AI協助資料整理、程式、文字與版面，AI不列為作者。"
            "作者負責來源核對、假設、解讀、公開與後續更正。",
            meta_style,
        ),
        PageBreak(),
    ]
)


def table_column_widths(column_count: int) -> list[float]:
    if column_count == 1:
        weights = [1.0]
    elif column_count == 2:
        weights = [0.38, 0.62]
    elif column_count == 3:
        weights = [0.28, 0.36, 0.36]
    else:
        weights = [1.25, *([1.0] * (column_count - 1))]
    total = sum(weights)
    return [doc.width * weight / total for weight in weights]


def add_table(rows: list[list[str]]) -> None:
    if not rows or not rows[0]:
        return
    column_count = len(rows[0])
    font_size = 7.1 if column_count <= 4 else 6.3 if column_count <= 6 else 5.5
    leading = font_size * 1.38
    cell_style = ParagraphStyle(
        f"CellTC{column_count}",
        parent=body,
        fontSize=font_size,
        leading=leading,
        alignment=TA_LEFT,
        spaceAfter=0,
        wordWrap="CJK",
    )
    normalized_rows: list[list[str]] = []
    for row in rows:
        padded = [*row, *([""] * max(0, column_count - len(row)))]
        normalized_rows.append(padded[:column_count])
    formatted = [
        [Paragraph(paragraph_html(cell, preserve_lines=True), cell_style) for cell in row]
        for row in normalized_rows
    ]
    table = LongTable(
        formatted,
        colWidths=table_column_widths(column_count),
        repeatRows=1,
        splitByRow=1,
        splitInRow=1,
        hAlign="LEFT",
    )
    table.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#f5eadf")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#4d2b1b")),
                ("GRID", (0, 0), (-1, -1), 0.35, colors.HexColor("#d9cbbd")),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("LEFTPADDING", (0, 0), (-1, -1), 3.5),
                ("RIGHTPADDING", (0, 0), (-1, -1), 3.5),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]
        )
    )
    story.extend([table, Spacer(1, 4 * mm)])


def add_formula_box(lines: list[str]) -> None:
    text = "<br/>".join(paragraph_html(line) for line in lines if line.strip())
    if not text:
        return
    box = Table([[Paragraph(text, formula_style)]], colWidths=[doc.width])
    box.setStyle(
        TableStyle(
            [
                ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f6f3ef")),
                ("BOX", (0, 0), (-1, -1), 0.45, colors.HexColor("#d5ccc4")),
                ("LEFTPADDING", (0, 0), (-1, -1), 7),
                ("RIGHTPADDING", (0, 0), (-1, -1), 7),
                ("TOPPADDING", (0, 0), (-1, -1), 6),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 6),
            ]
        )
    )
    story.extend([box, Spacer(1, 3 * mm)])


reference_titles = {
    title
    for title, _ in SECTIONS
    if title.startswith("參考文獻") or title.startswith("資料來源")
}

for section_index, (section_title, lines) in enumerate(SECTIONS):
    number_match = re.match(r"^(\d+)\.\s", section_title)
    if number_match and int(number_match.group(1)) in {4, 7}:
        story.append(PageBreak())
    elif section_index > 0:
        story.append(CondPageBreak(42 * mm))
    story.append(Paragraph(escape(ascii_dashes(section_title)), section_style))

    i = 0
    paragraph_buffer: list[str] = []

    def flush_pdf_paragraph() -> None:
        if paragraph_buffer:
            text = " ".join(paragraph_buffer)
            story.append(Paragraph(paragraph_html(text), body))
            paragraph_buffer.clear()

    while i < len(lines):
        line = lines[i].strip()
        if not line:
            flush_pdf_paragraph()
            i += 1
            continue
        if line.startswith("### "):
            flush_pdf_paragraph()
            story.append(
                Paragraph(paragraph_html(line[4:]), subsection_style)
            )
            i += 1
            continue
        image_match = re.match(r"!\[([^\]]+)\]\(([^)]+)\)", line)
        if image_match:
            flush_pdf_paragraph()
            image_path = DIR / image_match.group(2)
            if not image_path.exists():
                raise FileNotFoundError(f"Figure referenced by paper.md is missing: {image_path}")
            image = Image(str(image_path))
            max_width = doc.width
            max_height = 152 * mm
            scale = min(
                max_width / image.imageWidth,
                max_height / image.imageHeight,
            )
            image.drawWidth = image.imageWidth * scale
            image.drawHeight = image.imageHeight * scale
            caption = Paragraph(
                paragraph_html(image_match.group(1)),
                caption_style,
            )
            story.append(KeepTogether([image, caption]))
            i += 1
            continue
        if line.startswith("|") and i + 1 < len(lines) and "---" in lines[i + 1]:
            flush_pdf_paragraph()
            rows, consumed = parse_table(lines[i:])
            add_table(rows)
            i += consumed
            continue
        if line.startswith("```"):
            flush_pdf_paragraph()
            i += 1
            code_lines: list[str] = []
            while i < len(lines) and not lines[i].strip().startswith("```"):
                code_lines.append(lines[i].rstrip())
                i += 1
            i += 1 if i < len(lines) else 0
            add_formula_box(code_lines)
            continue
        if line.startswith(">"):
            flush_pdf_paragraph()
            quote_lines: list[str] = []
            while i < len(lines) and lines[i].strip().startswith(">"):
                quote_lines.append(lines[i].strip().lstrip(">").strip())
                i += 1
            add_formula_box(quote_lines)
            continue
        ordered_match = re.match(r"^\d+\.\s+(.+)", line)
        if ordered_match:
            flush_pdf_paragraph()
            items: list[ListItem] = []
            list_style = (
                reference_style if section_title in reference_titles else body
            )
            while i < len(lines):
                match = re.match(r"^\d+\.\s+(.+)", lines[i].strip())
                if not match:
                    break
                items.append(
                    ListItem(
                        Paragraph(paragraph_html(match.group(1)), list_style),
                        leftIndent=10,
                    )
                )
                i += 1
            story.append(
                ListFlowable(
                    items,
                    bulletType="1",
                    start="1",
                    leftIndent=18,
                    bulletFontName="NotoTC",
                    bulletFontSize=8,
                )
            )
            continue
        if line.startswith("- "):
            flush_pdf_paragraph()
            items = []
            while i < len(lines) and lines[i].strip().startswith("- "):
                items.append(
                    ListItem(
                        Paragraph(paragraph_html(lines[i].strip()[2:]), body),
                        leftIndent=10,
                    )
                )
                i += 1
            story.append(
                ListFlowable(
                    items,
                    bulletType="bullet",
                    leftIndent=18,
                    bulletFontName="NotoTC",
                    bulletFontSize=8,
                )
            )
            continue
        paragraph_buffer.append(line)
        i += 1
    flush_pdf_paragraph()

doc.build(story)
print(
    json.dumps(
        {"pdf": str(PDF_PATH), "content": str(CONTENT_PATH)},
        ensure_ascii=False,
    )
)
