"""把沙盘推演报告的 md 内容结构化生成为 docx。

直接用 python-docx 1.2.0；解析一份 markdown 字符串，输出同内容结构的 .docx。
支持：
- H1 / H2 / H3 标题（含 # 前缀和 === / --- 风格的 Setext 标题）
- 段落（自动识别引用 > 和列表 -）
- GFM 表格（含表头分隔 |---|）
- 有序 / 无序列表（嵌套缩进 2 空格）
- 加粗 **text** / 行内代码 `text`
- 引用块 > ...
- 水平线 ---
"""
from __future__ import annotations

import re
from pathlib import Path

from docx import Document
from docx.enum.table import WD_ALIGN_VERTICAL
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
from docx.shared import Cm, Pt, RGBColor

# ---------- 通用样式 ----------
CN_FONT = "微软雅黑"
EN_FONT = "Calibri"


def set_run_font(run, size_pt: float, bold: bool = False, color: tuple[int, int, int] | None = None):
    run.font.name = EN_FONT
    rPr = run._element.get_or_add_rPr()
    rFonts = rPr.find(qn("w:rFonts"))
    if rFonts is None:
        rFonts = OxmlElement("w:rFonts")
        rPr.append(rFonts)
    rFonts.set(qn("w:eastAsia"), CN_FONT)
    rFonts.set(qn("w:ascii"), EN_FONT)
    rFonts.set(qn("w:hAnsi"), EN_FONT)
    run.font.size = Pt(size_pt)
    run.bold = bold
    if color is not None:
        run.font.color.rgb = RGBColor(*color)


def shade_cell(cell, hex_color: str = "F2F2F2"):
    tcPr = cell._tc.get_or_add_tcPr()
    shd = OxmlElement("w:shd")
    shd.set(qn("w:val"), "clear")
    shd.set(qn("w:color"), "auto")
    shd.set(qn("w:fill"), hex_color)
    tcPr.append(shd)


def set_cell_borders(cell):
    tcPr = cell._tc.get_or_add_tcPr()
    tcBorders = OxmlElement("w:tcBorders")
    for side in ("top", "left", "bottom", "right"):
        b = OxmlElement(f"w:{side}")
        b.set(qn("w:val"), "single")
        b.set(qn("w:sz"), "6")
        b.set(qn("w:space"), "0")
        b.set(qn("w:color"), "808080")
        tcBorders.append(b)
    tcPr.append(tcBorders)


# ---------- Markdown 解析 ----------
def parse_inline(text: str) -> list[tuple[str, dict]]:
    """返回 (片段, 样式) 列表；样式含 bold/code。"""
    tokens: list[tuple[str, dict]] = []
    pattern = re.compile(r"(\*\*[^*]+\*\*|`[^`]+`)")
    pos = 0
    for m in pattern.finditer(text):
        if m.start() > pos:
            tokens.append((text[pos:m.start()], {}))
        chunk = m.group(0)
        if chunk.startswith("**"):
            tokens.append((chunk[2:-2], {"bold": True}))
        else:
            tokens.append((chunk[1:-1], {"code": True}))
        pos = m.end()
    if pos < len(text):
        tokens.append((text[pos:], {}))
    return tokens


def write_runs(paragraph, text: str, size_pt: float = 11, bold_default: bool = False, color=None):
    for chunk, style in parse_inline(text):
        run = paragraph.add_run(chunk)
        set_run_font(run, size_pt, bold=style.get("bold", bold_default), color=color)
        if style.get("code"):
            rPr = run._element.get_or_add_rPr()
            shd = OxmlElement("w:shd")
            shd.set(qn("w:val"), "clear")
            shd.set(qn("w:color"), "auto")
            shd.set(qn("w:fill"), "F5F5F5")
            rPr.append(shd)


def add_paragraph(doc, text: str, size: float = 11, bold: bool = False,
                  align: int = WD_ALIGN_PARAGRAPH.LEFT, color=None, indent_cm: float = 0.0,
                  space_before: float = 0, space_after: float = 4):
    p = doc.add_paragraph()
    p.alignment = align
    if indent_cm:
        p.paragraph_format.left_indent = Cm(indent_cm)
    p.paragraph_format.space_before = Pt(space_before)
    p.paragraph_format.space_after = Pt(space_after)
    p.paragraph_format.line_spacing = 1.5
    if text:
        write_runs(p, text, size, bold, color)
    return p


def add_heading(doc, text: str, level: int):
    sizes = {1: 18, 2: 15, 3: 13}
    color = (31, 73, 125) if level == 1 else (46, 117, 181) if level == 2 else (68, 114, 196)
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.LEFT
    p.paragraph_format.space_before = Pt(14 if level == 1 else 10)
    p.paragraph_format.space_after = Pt(6)
    run = p.add_run(text)
    set_run_font(run, sizes[level], bold=True, color=color)
    return p


def add_hr(doc):
    p = doc.add_paragraph()
    pPr = p._p.get_or_add_pPr()
    pBdr = OxmlElement("w:pBdr")
    bottom = OxmlElement("w:bottom")
    bottom.set(qn("w:val"), "single")
    bottom.set(qn("w:sz"), "6")
    bottom.set(qn("w:space"), "1")
    bottom.set(qn("w:color"), "BFBFBF")
    pBdr.append(bottom)
    pPr.append(pBdr)


def add_quote(doc, text: str):
    p = doc.add_paragraph()
    p.paragraph_format.left_indent = Cm(0.6)
    p.paragraph_format.space_before = Pt(2)
    p.paragraph_format.space_after = Pt(4)
    p.paragraph_format.line_spacing = 1.5
    # 左侧色条
    pPr = p._p.get_or_add_pPr()
    pBdr = OxmlElement("w:pBdr")
    left = OxmlElement("w:left")
    left.set(qn("w:val"), "single")
    left.set(qn("w:sz"), "24")
    left.set(qn("w:space"), "8")
    left.set(qn("w:color"), "2E75B6")
    pBdr.append(left)
    pPr.append(pBdr)
    write_runs(p, text, 10.5, color=(89, 89, 89))


def add_table(doc, rows: list[list[str]]):
    if not rows:
        return
    cols = len(rows[0])
    table = doc.add_table(rows=len(rows), cols=cols)
    table.autofit = True
    table.alignment = WD_ALIGN_PARAGRAPH.CENTER
    for i, row in enumerate(rows):
        for j, cell_text in enumerate(row):
            cell = table.rows[i].cells[j]
            cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
            set_cell_borders(cell)
            if i == 0:
                shade_cell(cell, "2E75B6")
            elif i % 2 == 0:
                shade_cell(cell, "F2F7FB")
            cell.text = ""
            p = cell.paragraphs[0]
            p.alignment = WD_ALIGN_PARAGRAPH.LEFT
            p.paragraph_format.space_before = Pt(2)
            p.paragraph_format.space_after = Pt(2)
            write_runs(p, cell_text.strip(),
                       size_pt=10.5,
                       bold_default=(i == 0),
                       color=(255, 255, 255) if i == 0 else None)
    doc.add_paragraph()  # 表格后空一行


def add_list_item(doc, text: str, ordered: bool, index: int, indent_level: int = 0):
    bullet = f"{index}. " if ordered else "• "
    p = doc.add_paragraph()
    p.paragraph_format.left_indent = Cm(0.6 + indent_level * 0.6)
    p.paragraph_format.space_before = Pt(0)
    p.paragraph_format.space_after = Pt(2)
    p.paragraph_format.line_spacing = 1.4
    prefix_run = p.add_run(bullet)
    set_run_font(prefix_run, 10.5, bold=True, color=(46, 117, 181))
    write_runs(p, text, 10.5)


# ---------- 主流程 ----------
def md_to_docx(md_text: str, out_path: Path, title: str = ""):
    doc = Document()

    # 全局默认样式
    style = doc.styles["Normal"]
    style.font.name = EN_FONT
    style.font.size = Pt(11)
    rPr = style.element.get_or_add_rPr()
    rFonts = rPr.find(qn("w:rFonts"))
    if rFonts is None:
        rFonts = OxmlElement("w:rFonts")
        rPr.append(rFonts)
    rFonts.set(qn("w:eastAsia"), CN_FONT)
    rFonts.set(qn("w:ascii"), EN_FONT)
    rFonts.set(qn("w:hAnsi"), EN_FONT)

    # 页边距
    for section in doc.sections:
        section.top_margin = Cm(2.54)
        section.bottom_margin = Cm(2.54)
        section.left_margin = Cm(3.17)
        section.right_margin = Cm(3.17)

    # 封面
    if title:
        cover = doc.add_paragraph()
        cover.alignment = WD_ALIGN_PARAGRAPH.CENTER
        cover.paragraph_format.space_before = Pt(60)
        run = cover.add_run(title)
        set_run_font(run, 24, bold=True, color=(31, 73, 125))

    subtitle = doc.add_paragraph()
    subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
    subtitle.paragraph_format.space_before = Pt(12)
    sub_run = subtitle.add_run("商业沙盘推演 · 沙盘运行日期：2026-09-11")
    set_run_font(sub_run, 11, color=(89, 89, 89))

    line_meta = doc.add_paragraph()
    line_meta.alignment = WD_ALIGN_PARAGRAPH.CENTER
    line_meta.paragraph_format.space_before = Pt(8)
    line_run = line_meta.add_run("沙盘推演官视角｜区分客观推演与主观建议，不替用户决策")
    set_run_font(line_run, 10, color=(127, 127, 127))

    # 封面分隔线
    sep = doc.add_paragraph()
    sep.paragraph_format.space_before = Pt(48)
    sep_pPr = sep._p.get_or_add_pPr()
    sep_pBdr = OxmlElement("w:pBdr")
    sep_bottom = OxmlElement("w:bottom")
    sep_bottom.set(qn("w:val"), "single")
    sep_bottom.set(qn("w:sz"), "12")
    sep_bottom.set(qn("w:space"), "1")
    sep_bottom.set(qn("w:color"), "2E75B6")
    sep_pBdr.append(sep_bottom)
    sep_pPr.append(sep_pBdr)

    doc.add_page_break()

    # 逐行解析 markdown
    lines = md_text.split("\n")
    i = 0
    n = len(lines)
    while i < n:
        line = lines[i]
        stripped = line.strip()

        # 跳过空行（已被吸收进段落控制）
        if not stripped:
            i += 1
            continue

        # Setext H1/H2：=== / ---
        if i + 1 < n and lines[i + 1].strip() in ("===", "---") and stripped and not stripped.startswith(("#", "|", "-", ">", "*", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.", "0.")):
            level = 1 if lines[i + 1].strip() == "===" else 2
            add_heading(doc, stripped, level)
            i += 2
            continue

        # ATX 标题
        m = re.match(r"^(#{1,3})\s+(.+?)\s*#*\s*$", stripped)
        if m:
            level = len(m.group(1))
            add_heading(doc, m.group(2), level)
            i += 1
            continue

        # 水平线
        if re.match(r"^-{3,}$", stripped):
            add_hr(doc)
            i += 1
            continue

        # 表格：连续多行 | 开头 + 表头分隔
        if stripped.startswith("|") and i + 1 < n and re.match(r"^\|?[\s:|-]+\|?$", lines[i + 1].strip()):
            rows: list[list[str]] = []
            while i < n and lines[i].strip().startswith("|"):
                row_cells = [c for c in lines[i].strip().strip("|").split("|")]
                rows.append(row_cells)
                i += 1
            # 跳过表头分隔
            if len(rows) >= 2 and re.match(r"^[\s:|-]+$", "|".join(rows[1])):
                rows.pop(1)
            add_table(doc, rows)
            continue

        # 引用
        if stripped.startswith(">"):
            buf = stripped.lstrip(">").strip()
            # 收集连续引用行
            i += 1
            while i < n and lines[i].strip().startswith(">"):
                buf += " " + lines[i].strip().lstrip(">").strip()
                i += 1
            add_quote(doc, buf)
            continue

        # 列表
        ordered = bool(re.match(r"^\d+\.\s+", stripped))
        unordered = stripped.startswith("- ") or stripped.startswith("* ")
        if ordered or unordered:
            idx = 1
            while i < n:
                l = lines[i]
                ls = l.strip()
                if not ls:
                    break
                m_ord = re.match(r"^(\d+)\.\s+(.*)$", ls)
                m_unord = re.match(r"^[-*]\s+(.*)$", ls)
                if m_ord:
                    indent = (len(l) - len(l.lstrip())) // 2
                    add_list_item(doc, m_ord.group(2), ordered=True, index=int(m_ord.group(1)), indent_level=indent)
                    i += 1
                elif m_unord:
                    indent = (len(l) - len(l.lstrip())) // 2
                    add_list_item(doc, m_unord.group(1), ordered=False, index=0, indent_level=indent)
                    i += 1
                else:
                    break
            continue

        # 普通段落：合并连续非空行
        para_lines = [stripped]
        i += 1
        while i < n and lines[i].strip() and not (
            lines[i].strip().startswith(("#", "|", ">", "-", "*"))
            or re.match(r"^\d+\.\s+", lines[i].strip())
            or re.match(r"^-{3,}$", lines[i].strip())
        ):
            para_lines.append(lines[i].strip())
            i += 1
        add_paragraph(doc, " ".join(para_lines), size=11, space_after=6)

    doc.save(out_path)


if __name__ == "__main__":
    import sys
    src = Path(sys.argv[1])
    dst = Path(sys.argv[2])
    title = sys.argv[3] if len(sys.argv) > 3 else src.stem
    md_to_docx(src.read_text(encoding="utf-8"), dst, title)
    print(f"OK: {dst}")