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]]] = []
    current_title: str | None = None
    current_lines: list[str] = []
    for line in MARKDOWN.splitlines():
        if line.startswith("## "):
            if current_title is not None:
                sections.append((current_title, current_lines))
            current_title = inline_text(line[3:])
            current_lines = []
        elif current_title is not None:
            current_lines.append(line)
    if current_title is not None:
        sections.append((current_title, current_lines))
    return sections


SECTIONS = split_sections()


def find_section(name: str) -> list[str]:
    for title, lines in SECTIONS:
        if title == name:
            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(item) for item in result if inline_text(item)]


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_match = re.match(r"!\[([^]]+)\]\(([^)]+)\)", line)
        if image_match:
            flush()
            blocks.append({
                "type": "figure",
                "filename": Path(image_match.group(2)).name,
                "caption": inline_text(image_match.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
        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
        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
        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 not match:
            continue
        label = inline_text(match.group(1))
        url_match = re.search(r"https?://\S+", label)
        references.append([label, url_match.group(0).rstrip(".,") if url_match 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])))
]

content = {
    "reportNumber": META["reportNumber"],
    "version": META["version"],
    "date": META["datePublished"],
    "revisionDate": META["dateModified"],
    "revisionNote": META["revisionNote"],
    "doi": META["doi"],
    "zenodoUrl": META["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": [
        ["120", "完整月觀察"],
        ["289.705 TWh", "2025全國毛發電"],
        ["13.3229%", "2025再生能源占比"],
        ["4", "官方係數口徑"],
    ],
    "sections": web_sections,
    "references": references,
}
(ROOT / "paper-content.json").write_text(json.dumps(content, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


rl_config.invariant = 1
FONT_PATH = choose_font()
pdfmetrics.registerFont(TTFont("NotoTC", str(FONT_PATH)))
styles = getSampleStyleSheet()
body = ParagraphStyle("BodyTC", fontName="NotoTC", fontSize=8.8, leading=14.5, alignment=TA_JUSTIFY, textColor=colors.HexColor("#241c17"), spaceAfter=6, allowWidows=0, allowOrphans=0, wordWrap="CJK")
title_style = ParagraphStyle("TitleTC", parent=body, fontSize=18.5, leading=28, alignment=TA_CENTER, spaceAfter=11)
section_style = ParagraphStyle("SectionTC", parent=body, fontSize=14.2, leading=21, textColor=colors.HexColor("#6f3519"), spaceBefore=10, spaceAfter=7, keepWithNext=True)
subsection_style = ParagraphStyle("SubsectionTC", parent=body, fontSize=11.2, leading=17, textColor=colors.HexColor("#35261e"), spaceBefore=7, spaceAfter=4, 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.3, leading=10.2, alignment=TA_CENTER, textColor=colors.HexColor("#625b55"), spaceAfter=7)
reference_style = ParagraphStyle("ReferenceTC", parent=body, fontSize=7.3, leading=10.2, 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']}"
        if len(header) > 52:
            header = header[:51] + "..."
        canvas.drawString(16 * mm, A4[1] - 9 * mm, header)
        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["title"],
    author=META["author"]["name"], subject="2025年臺灣電力排碳係數分流與發電結構之官方資料分析",
    keywords=", ".join(META["keywords"]),
)
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/>"
)
story: list[object] = [
    Spacer(1, 8 * mm),
    Paragraph(f"{META['reportNumber']}｜公開工作論文｜版本 {META['version']}", meta_style),
    Spacer(1, 9 * mm),
    Paragraph(paragraph_html(META["title"]), title_style),
    Paragraph(paragraph_html(META["titleEnglish"]), meta_style),
    Spacer(1, 8 * 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/>"
        + doi_line
        + "授權：CC BY 4.0（僅適用於作者有權授權的原創內容；第三方內容另依原權利條款）",
        meta_style,
    ),
    Spacer(1, 9 * mm),
]

summary = abstract_zh.split("\n\n")[0]
if len(summary) > 430:
    summary = summary[:429].rstrip("，、；： ") + "..."
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 = Table([[Paragraph(
    "<b>解讀邊界</b><br/>三個正式係數與一個初估全國係數有不同分母、交易範圍及適用對象，不能互換。"
    "本文描述官方彙總資料，不識別政策因果、不評價個別用戶，也不替代盤查、查驗、法律或投資判斷。", 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])
    cell_style = ParagraphStyle("Cell", parent=body, fontSize=6.6 if columns >= 5 else 7.1, leading=9.6 if columns >= 5 else 10.4, spaceAfter=0, alignment=TA_LEFT)
    formatted = [[Paragraph(paragraph_html(cell), cell_style) for cell 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), 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, (title, lines) in enumerate(SECTIONS):
    if re.match(r"^(1|4|7|9)\.\s", title):
        story.append(PageBreak())
    elif section_index:
        story.append(CondPageBreak(38 * mm))
    story.append(Paragraph(paragraph_html(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
            story.append(KeepTogether([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_lines: list[str] = []
            while index < len(lines) and not lines[index].strip().startswith("```"):
                if lines[index].strip():
                    code_lines.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_lines), 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()
            items: list[ListItem] = []
            style = reference_style if title.startswith("參考文獻") else body
            while index < len(lines):
                match = re.match(r"^\d+\.\s+(.+)", lines[index].strip())
                if not match:
                    break
                items.append(ListItem(Paragraph(paragraph_html(match.group(1)), style), leftIndent=10))
                index += 1
            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))
