from __future__ import annotations

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

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


ROOT = Path(__file__).resolve().parent
FONT = ROOT / "assets" / "fonts" / "NotoSansTC-Regular.ttf"
FONT_SHA256 = "40452f54ea36b5c1f1ec8bd92dc18d5cd19fafb73b8abf9c48500a00d09ec762"
META = json.loads((ROOT / "paper-metadata.json").read_text(encoding="utf-8"))
RESULTS = json.loads((ROOT / "outputs" / "analysis-results.json").read_text(encoding="utf-8"))
MARKDOWN = (ROOT / "paper.md").read_text(encoding="utf-8").replace("\r\n", "\n")


def choose_font() -> Path:
    if not FONT.is_file():
        raise FileNotFoundError(f"Packaged Noto Sans TC font is missing: {FONT}")
    actual = hashlib.sha256(FONT.read_bytes()).hexdigest()
    if actual != FONT_SHA256:
        raise ValueError(f"Packaged font hash mismatch: {actual}")
    return FONT


def ascii_dashes(text: str) -> str:
    for value in ("\u2010", "\u2011", "\u2012", "\u2013", "\u2014", "\u2015", "\u2212"):
        text = text.replace(value, "-")
    return text


def inline_text(text: str) -> str:
    text = ascii_dashes(text.strip())
    text = re.sub(r"!\[([^]]+)\]\([^)]+\)", r"\1", text)
    text = re.sub(r"\[([^]]+)\]\((https?://[^)]+)\)", r"\1 (\2)", text)
    text = text.replace("**", "").replace("`", "")
    return re.sub(r"\s+", " ", text).strip()


def paragraph_html(text: str) -> str:
    text = ascii_dashes(text.strip())
    links: list[tuple[str, str]] = []

    def capture_link(match: re.Match[str]) -> str:
        links.append((match.group(1), match.group(2)))
        return f"@@LINK{len(links) - 1}@@"

    text = re.sub(r"\[([^]]+)\]\((https?://[^)]+)\)", capture_link, text)
    text = escape(text)
    text = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", text)
    text = re.sub(r"`([^`]+)`", r"<font name='Courier'>\1</font>", text)
    for index, (label, url) in enumerate(links):
        text = text.replace(f"@@LINK{index}@@", f"<a href='{escape(url)}'>{escape(label)}</a>")
    return text


def split_sections() -> list[tuple[str, list[str]]]:
    sections: list[tuple[str, list[str]]] = []
    title: str | None = None
    lines: list[str] = []
    for raw in MARKDOWN.splitlines():
        if raw.startswith("## "):
            if title is not None:
                sections.append((title, lines))
            title = inline_text(raw[3:])
            lines = []
        elif title is not None:
            lines.append(raw)
    if title is not None:
        sections.append((title, lines))
    return sections


SECTIONS = split_sections()


def find_section(name: str) -> list[str]:
    for title, lines in SECTIONS:
        if title.casefold() == name.casefold():
            return lines
    raise ValueError(f"Missing section: {name}")


def paragraphs(lines: list[str]) -> list[str]:
    result: list[str] = []
    buffer: list[str] = []
    for raw in lines:
        line = raw.strip()
        if not line:
            if buffer:
                result.append(" ".join(buffer))
                buffer = []
            continue
        if line.startswith(("### ", "- ", "|", "```", "![")) or re.match(r"^\d+\.\s", line):
            if buffer:
                result.append(" ".join(buffer))
                buffer = []
            continue
        buffer.append(line)
    if buffer:
        result.append(" ".join(buffer))
    return [inline_text(value) for value in result if inline_text(value)]


def parse_table(lines: list[str], start: int) -> tuple[list[list[str]], int]:
    rows: list[list[str]] = []
    index = start
    while index < len(lines) and lines[index].strip().startswith("|"):
        raw = lines[index].strip()
        cells = [inline_text(cell) for cell in raw.strip("|").split("|")]
        if not all(re.fullmatch(r":?-{3,}:?", cell.replace(" ", "")) for cell in cells):
            rows.append(cells)
        index += 1
    return rows, index - start


def parse_web_blocks(lines: list[str]) -> list[dict[str, object]]:
    blocks: list[dict[str, object]] = []
    buffer: list[str] = []
    index = 0

    def flush() -> None:
        if buffer:
            blocks.append({"type": "paragraph", "text": inline_text(" ".join(buffer))})
            buffer.clear()

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


abstract_zh = "\n\n".join(paragraphs(find_section("摘要")))
abstract_en = "\n\n".join(paragraphs(find_section("Abstract")))
if not abstract_zh or not abstract_en:
    raise ValueError("Both Chinese and English abstracts are required")

references: list[list[str]] = []
for title, lines in SECTIONS:
    if not title.startswith("參考文獻"):
        continue
    for line in lines:
        match = re.match(r"^\d+\.\s+(.+)", line.strip())
        if match:
            label = inline_text(match.group(1))
            url = re.search(r"https?://\S+", label)
            references.append([label, url.group(0).rstrip(".,") if url else ""])

web_sections = [
    {"id": f"section-{index + 1}", "title": title, "blocks": parse_web_blocks(lines)}
    for index, (title, lines) in enumerate(item for item in SECTIONS if re.match(r"^\d+\.\s", item[0]))
]
counts = RESULTS["designCounts"]
content = {
    "reportNumber": META["reportNumber"],
    "version": META["version"],
    "date": META["datePublished"],
    "revisionDate": META["dateModified"],
    "revisionNote": META["revisionNote"],
    "doi": META.get("doi"),
    "zenodoUrl": META.get("zenodoUrl"),
    "status": META["status"],
    "license": META["license"],
    "licenseUrl": META["licenseUrl"],
    "titleZh": META["title"],
    "titleEn": META["titleEnglish"],
    "author": META["author"]["name"],
    "authorEn": META["author"]["nameEnglish"],
    "orcid": META["author"]["orcid"],
    "email": META["author"]["email"],
    "publicContactConsent": META.get("publicContactConsent", False),
    "abstractZh": abstract_zh,
    "abstractEn": abstract_en,
    "keywords": META["keywords"],
    "highlights": [
        [str(counts["independentFields"]), "獨立田區"],
        [str(counts["independentFixedPlots"]), "固定樣區"],
        [str(counts["plotDateRepeatedObservations"]), "樣區-日期"],
        [str(counts["totalPlannedFlights"]), "規劃航次"],
    ],
    "fieldDataCollected": META.get("fieldDataCollected", False),
    "sections": web_sections,
    "references": references,
}
(ROOT / "paper-content.json").write_text(json.dumps(content, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def yaml_quote(value: object) -> str:
    return json.dumps(value, ensure_ascii=False)


def write_citation() -> None:
    lines = [
        "cff-version: 1.2.0",
        f"message: {yaml_quote('若使用本研究，請引用以下資料。')}",
        f"title: {yaml_quote(META['title'])}",
        "type: dataset",
        "authors:",
        f"  - family-names: {yaml_quote('周')}",
        f"    given-names: {yaml_quote('秉漢')}",
        f"    alias: {yaml_quote(META['author']['nameEnglish'])}",
        f"    orcid: {yaml_quote('https://orcid.org/' + META['author']['orcid'])}",
        f"version: {yaml_quote(META['version'])}",
        f"date-released: {META['datePublished']}",
        f"url: {yaml_quote(META['url'])}",
    ]
    if META.get("doi"):
        lines.extend([
            f"doi: {yaml_quote(META['doi'])}",
            "identifiers:",
            "  - type: doi",
            f"    value: {yaml_quote(META['doi'])}",
            f"    description: {yaml_quote('Version DOI')}",
        ])
    lines.extend([
        "  - type: url" if META.get("doi") else "identifiers:\n  - type: url",
        f"    value: {yaml_quote(META['url'] + '/SHWRP-2026-031-v' + META['version'] + '.zip')}",
        f"    description: {yaml_quote('Versioned reproducibility package')}",
        "license: CC-BY-4.0",
        f"abstract: {yaml_quote(META['description'])}",
        "keywords:",
    ])
    lines.extend(f"  - {yaml_quote(keyword)}" for keyword in META["keywords"])
    lines.extend([
        "preferred-citation:",
        "  type: report",
        f"  title: {yaml_quote(META['title'])}",
        "  authors:",
        f"    - family-names: {yaml_quote('周')}",
        f"      given-names: {yaml_quote('秉漢')}",
        "  year: 2026",
        "  month: 8",
        f"  number: {yaml_quote(META['reportNumber'])}",
        "  institution:",
        f"    name: {yaml_quote('上好味研究工作論文系列')}",
        f"  version: {yaml_quote(META['version'])}",
    ])
    if META.get("doi"):
        lines.append(f"  doi: {yaml_quote(META['doi'])}")
    lines.append(f"  url: {yaml_quote(META['url'])}")
    (ROOT / "CITATION.cff").write_text("\n".join(lines) + "\n", encoding="utf-8")


write_citation()


rl_config.invariant = 1
pdfmetrics.registerFont(TTFont("NotoTC", str(choose_font())))
styles = getSampleStyleSheet()
body = ParagraphStyle("BodyTC", fontName="NotoTC", fontSize=8.7, leading=14.3, alignment=TA_JUSTIFY, textColor=colors.HexColor("#241c17"), spaceAfter=6, allowWidows=0, allowOrphans=0, wordWrap="CJK")
title_style = ParagraphStyle("TitleTC", parent=body, fontSize=18.2, leading=27, alignment=TA_CENTER, spaceAfter=10)
section_style = ParagraphStyle("SectionTC", parent=body, fontSize=14, leading=20.5, textColor=colors.HexColor("#6f3519"), spaceBefore=10, spaceAfter=7, keepWithNext=True)
subsection_style = ParagraphStyle("SubsectionTC", parent=body, fontSize=11.1, leading=17, textColor=colors.HexColor("#35261e"), spaceBefore=7, spaceAfter=4, keepWithNext=True)
meta_style = ParagraphStyle("MetaTC", parent=body, fontSize=8.1, leading=13, alignment=TA_CENTER, textColor=colors.HexColor("#625b55"))
caption_style = ParagraphStyle("CaptionTC", parent=body, fontSize=7.2, leading=10.2, alignment=TA_CENTER, textColor=colors.HexColor("#625b55"), spaceAfter=7)
reference_style = ParagraphStyle("ReferenceTC", parent=body, fontSize=7.2, leading=10.1, alignment=TA_LEFT, spaceAfter=1.5)
formula_style = ParagraphStyle("FormulaTC", parent=body, fontSize=8.1, leading=13, alignment=TA_LEFT, textColor=colors.HexColor("#3c3029"))


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


doc = BaseDocTemplate(
    str(ROOT / "paper.pdf"), pagesize=A4, leftMargin=16 * mm, rightMargin=16 * mm,
    topMargin=17 * mm, bottomMargin=17 * mm,
    title=META["titleEnglish"],
    author=META["author"]["nameEnglish"],
    subject="Prospective minimum viable validation protocol for DJI Mavic 3 Multispectral rice scouting in Taiwan",
    keywords="DJI Mavic 3 Multispectral, agricultural drone, rice scouting, multispectral imaging, ground validation, prospective protocol, Taiwan",
)
frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="normal")
doc.addPageTemplates([PageTemplate(id="paper", frames=[frame], onPage=header_footer)])
doi_line = f"DOI：<link href='https://doi.org/{escape(META['doi'])}'>{escape(META['doi'])}</link><br/>" if META.get("doi") else "DOI：尚未建立<br/>"
summary = abstract_zh.split("\n\n")[0]
if len(summary) > 440:
    summary = summary[:439].rstrip("，、；： ") + "..."
story: list[object] = [
    Spacer(1, 7 * mm),
    Paragraph(f"{META['reportNumber']}｜公開工作論文｜版本 {META['version']}", meta_style),
    Spacer(1, 8 * mm),
    Paragraph(paragraph_html(META["title"]), title_style),
    Paragraph(paragraph_html(META["titleEnglish"]), meta_style),
    Spacer(1, 7 * 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/>公開日期：{META['datePublished']}｜未經外部同儕審查<br/>"
        + doi_line + "授權：CC BY 4.0（僅適用於作者有權授權的原創內容；第三方內容另依原權利條款）",
        meta_style,
    ),
    Spacer(1, 8 * mm),
]
cover = Table([[Paragraph("<b>研究摘要</b><br/><br/>" + paragraph_html(summary), body)]], colWidths=[155 * mm])
cover.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), 8 * mm), ("RIGHTPADDING", (0, 0), (-1, -1), 8 * mm), ("TOPPADDING", (0, 0), (-1, -1), 5 * mm), ("BOTTOMPADDING", (0, 0), (-1, -1), 5 * mm)]))
boundary_text = (
    "<b>證據邊界</b><br/>本版本是公開證據綜整與未來實飛協定，尚未採集田間資料；所有效能、重複性、成本與投資報酬均為待驗證。"
    "植生指標只用於相對異常排序與地面查核優先序，不構成病害、缺氮、缺水、農藥或施肥處方。"
)
boundary = Table([[Paragraph(boundary_text, formula_style)]], colWidths=[155 * mm])
boundary.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, Spacer(1, 5 * mm), boundary, Spacer(1, 6 * mm), Paragraph("AI協助來源整理、程式、圖表、語言與版面；AI不列為作者。作者負責核對、解讀、公開與更正。", meta_style), PageBreak()])


def add_table(rows: list[list[str]]) -> None:
    if not rows:
        return
    columns = len(rows[0])
    if any(len(row) != columns for row in rows):
        raise ValueError("Markdown table has inconsistent column counts")
    cell = ParagraphStyle("Cell", parent=body, fontSize=6.4 if columns >= 5 else 7.0, leading=9.4 if columns >= 5 else 10.3, spaceAfter=0, alignment=TA_LEFT)
    formatted = [[Paragraph(paragraph_html(value), cell) for value in row] for row in rows]
    table = LongTable(formatted, colWidths=[doc.width / columns] * columns, repeatRows=1, splitByRow=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.2), ("RIGHTPADDING", (0, 0), (-1, -1), 3.2), ("TOPPADDING", (0, 0), (-1, -1), 3.5), ("BOTTOMPADDING", (0, 0), (-1, -1), 3.5)]))
    story.extend([table, Spacer(1, 3 * mm)])


for section_index, (section_title, lines) in enumerate(SECTIONS):
    if re.match(r"^(1|4|7|10|11)\.\s", section_title) or section_title.startswith(("參考文獻", "研究揭露")):
        story.append(PageBreak())
        if section_title.startswith("參考文獻"):
            story.append(Spacer(1, 12 * mm))
    elif section_index:
        story.append(CondPageBreak(38 * mm))
    story.append(Paragraph(paragraph_html(section_title), section_style))
    index = 0
    buffer: list[str] = []

    def flush_paragraph() -> None:
        if buffer:
            story.append(Paragraph(paragraph_html(" ".join(buffer)), body))
            buffer.clear()

    while index < len(lines):
        line = lines[index].strip()
        if not line:
            flush_paragraph()
            index += 1
            continue
        if line.startswith("### "):
            flush_paragraph()
            story.append(Paragraph(paragraph_html(line[4:]), subsection_style))
            index += 1
            continue
        image_match = re.match(r"!\[([^]]+)\]\(([^)]+)\)", line)
        if image_match:
            flush_paragraph()
            image_path = ROOT / image_match.group(2)
            if not image_path.is_file():
                raise FileNotFoundError(image_path)
            image = Image(str(image_path))
            scale = min(doc.width / image.imageWidth, 130 * mm / image.imageHeight)
            image.drawWidth = image.imageWidth * scale
            image.drawHeight = image.imageHeight * scale
            if image_path.name == "figure-6-normalized-cost-sensitivity.png":
                story.append(PageBreak())
            story.extend([
                CondPageBreak(image.drawHeight + 14 * mm),
                image,
                Paragraph(paragraph_html(image_match.group(1)), caption_style),
            ])
            index += 1
            continue
        if line.startswith("|") and index + 1 < len(lines) and "---" in lines[index + 1]:
            flush_paragraph()
            rows, consumed = parse_table(lines, index)
            add_table(rows)
            index += consumed
            continue
        if line.startswith("```"):
            flush_paragraph()
            index += 1
            code: list[str] = []
            while index < len(lines) and not lines[index].strip().startswith("```"):
                if lines[index].strip():
                    code.append(lines[index].strip())
                index += 1
            index += 1 if index < len(lines) else 0
            box = Table([[Paragraph("<br/>".join(paragraph_html(item) for item in code), 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)])
            continue
        ordered = re.match(r"^\d+\.\s+(.+)", line)
        if ordered:
            flush_paragraph()
            numbered_values: list[str] = []
            while index < len(lines):
                match = re.match(r"^\d+\.\s+(.+)", lines[index].strip())
                if not match:
                    break
                numbered_values.append(match.group(1))
                index += 1
            if section_title.startswith("參考文獻"):
                for item_number, value in enumerate(numbered_values, 1):
                    story.append(Paragraph(f"{item_number}. {paragraph_html(value)}", reference_style))
            else:
                items = [ListItem(Paragraph(paragraph_html(value), body), leftIndent=10) for value in numbered_values]
                story.append(ListFlowable(items, bulletType="1", start="1", leftIndent=18, bulletFontName="NotoTC", bulletFontSize=8))
            continue
        if line.startswith("- "):
            flush_paragraph()
            items = []
            while index < len(lines) and lines[index].strip().startswith("- "):
                items.append(ListItem(Paragraph(paragraph_html(lines[index].strip()[2:]), body), leftIndent=10))
                index += 1
            story.append(ListFlowable(items, bulletType="bullet", leftIndent=18, bulletFontName="NotoTC", bulletFontSize=8))
            continue
        buffer.append(line)
        index += 1
    flush_paragraph()

doc.build(story)
print(json.dumps({"pdf": str(ROOT / "paper.pdf"), "content": str(ROOT / "paper-content.json")}, ensure_ascii=False))
