#!/usr/bin/env python3
from __future__ import annotations

import re
import sys
from copy import deepcopy
from pathlib import Path

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Inches, Pt, RGBColor


NAVY = "1F4E79"
ORANGE = "E87520"
INK = "222222"
MUTED = "667085"
PALE_BLUE = "EAF1F6"
PALE_ORANGE = "FFF3E8"
WHITE = "FFFFFF"
FONT_CN = "Hiragino Sans GB"
FONT_LATIN = "Arial"


def set_run_font(run, size=None, bold=None, color=None, name_cn=FONT_CN, name_latin=FONT_LATIN):
    run.font.name = name_latin
    run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name_cn)
    run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), name_latin)
    run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), name_latin)
    if size is not None:
        run.font.size = Pt(size)
    if bold is not None:
        run.bold = bold
    if color:
        run.font.color.rgb = RGBColor.from_string(color)


def shade_cell(cell, fill):
    tc_pr = cell._tc.get_or_add_tcPr()
    shd = tc_pr.find(qn("w:shd"))
    if shd is None:
        shd = OxmlElement("w:shd")
        tc_pr.append(shd)
    shd.set(qn("w:fill"), fill)


def set_cell_margins(cell, top=110, start=120, bottom=110, end=120):
    tc = cell._tc
    tc_pr = tc.get_or_add_tcPr()
    tc_mar = tc_pr.first_child_found_in("w:tcMar")
    if tc_mar is None:
        tc_mar = OxmlElement("w:tcMar")
        tc_pr.append(tc_mar)
    for m, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
        node = tc_mar.find(qn(f"w:{m}"))
        if node is None:
            node = OxmlElement(f"w:{m}")
            tc_mar.append(node)
        node.set(qn("w:w"), str(value))
        node.set(qn("w:type"), "dxa")


def set_repeat_table_header(row):
    tr_pr = row._tr.get_or_add_trPr()
    tbl_header = OxmlElement("w:tblHeader")
    tbl_header.set(qn("w:val"), "true")
    tr_pr.append(tbl_header)


def prevent_row_split(row):
    tr_pr = row._tr.get_or_add_trPr()
    cant_split = OxmlElement("w:cantSplit")
    cant_split.set(qn("w:val"), "true")
    tr_pr.append(cant_split)


def ensure_numbering(doc, fmt, text, left=420, hanging=240):
    numbering = doc.part.numbering_part.element
    abstract_ids = [int(el.get(qn("w:abstractNumId"))) for el in numbering.findall(qn("w:abstractNum"))]
    num_ids = [int(el.get(qn("w:numId"))) for el in numbering.findall(qn("w:num"))]
    abstract_id = max(abstract_ids, default=-1) + 1
    num_id = max(num_ids, default=0) + 1

    abstract = OxmlElement("w:abstractNum")
    abstract.set(qn("w:abstractNumId"), str(abstract_id))
    multi = OxmlElement("w:multiLevelType")
    multi.set(qn("w:val"), "singleLevel")
    abstract.append(multi)
    lvl = OxmlElement("w:lvl")
    lvl.set(qn("w:ilvl"), "0")
    start = OxmlElement("w:start")
    start.set(qn("w:val"), "1")
    lvl.append(start)
    num_fmt = OxmlElement("w:numFmt")
    num_fmt.set(qn("w:val"), fmt)
    lvl.append(num_fmt)
    lvl_text = OxmlElement("w:lvlText")
    lvl_text.set(qn("w:val"), text)
    lvl.append(lvl_text)
    suff = OxmlElement("w:suff")
    suff.set(qn("w:val"), "tab")
    lvl.append(suff)
    p_pr = OxmlElement("w:pPr")
    tabs = OxmlElement("w:tabs")
    tab = OxmlElement("w:tab")
    tab.set(qn("w:val"), "num")
    tab.set(qn("w:pos"), str(left))
    tabs.append(tab)
    p_pr.append(tabs)
    ind = OxmlElement("w:ind")
    ind.set(qn("w:left"), str(left))
    ind.set(qn("w:hanging"), str(hanging))
    p_pr.append(ind)
    lvl.append(p_pr)
    abstract.append(lvl)
    numbering.append(abstract)

    num = OxmlElement("w:num")
    num.set(qn("w:numId"), str(num_id))
    abstract_ref = OxmlElement("w:abstractNumId")
    abstract_ref.set(qn("w:val"), str(abstract_id))
    num.append(abstract_ref)
    numbering.append(num)
    return num_id


def apply_numbering(paragraph, num_id):
    p_pr = paragraph._p.get_or_add_pPr()
    num_pr = OxmlElement("w:numPr")
    ilvl = OxmlElement("w:ilvl")
    ilvl.set(qn("w:val"), "0")
    num = OxmlElement("w:numId")
    num.set(qn("w:val"), str(num_id))
    num_pr.append(ilvl)
    num_pr.append(num)
    p_pr.append(num_pr)


def set_table_borders(table, color="CDD5DF", size="6"):
    tbl_pr = table._tbl.tblPr
    borders = tbl_pr.find(qn("w:tblBorders"))
    if borders is None:
        borders = OxmlElement("w:tblBorders")
        tbl_pr.append(borders)
    for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
        node = borders.find(qn(f"w:{edge}"))
        if node is None:
            node = OxmlElement(f"w:{edge}")
            borders.append(node)
        node.set(qn("w:val"), "single")
        node.set(qn("w:sz"), size)
        node.set(qn("w:color"), color)


def set_table_geometry(table, widths_inches):
    table.autofit = False
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    dxa_widths = [int(w * 1440) for w in widths_inches]
    total = sum(dxa_widths)
    tbl_pr = table._tbl.tblPr
    tbl_w = tbl_pr.find(qn("w:tblW"))
    if tbl_w is None:
        tbl_w = OxmlElement("w:tblW")
        tbl_pr.append(tbl_w)
    tbl_w.set(qn("w:w"), str(total))
    tbl_w.set(qn("w:type"), "dxa")
    tbl_layout = tbl_pr.find(qn("w:tblLayout"))
    if tbl_layout is None:
        tbl_layout = OxmlElement("w:tblLayout")
        tbl_pr.append(tbl_layout)
    tbl_layout.set(qn("w:type"), "fixed")

    tbl_grid = table._tbl.tblGrid
    for child in list(tbl_grid):
        tbl_grid.remove(child)
    for dxa in dxa_widths:
        grid_col = OxmlElement("w:gridCol")
        grid_col.set(qn("w:w"), str(dxa))
        tbl_grid.append(grid_col)

    for row in table.rows:
        for idx, cell in enumerate(row.cells):
            width = dxa_widths[min(idx, len(dxa_widths) - 1)]
            tc_pr = cell._tc.get_or_add_tcPr()
            tc_w = tc_pr.find(qn("w:tcW"))
            if tc_w is None:
                tc_w = OxmlElement("w:tcW")
                tc_pr.append(tc_w)
            tc_w.set(qn("w:w"), str(width))
            tc_w.set(qn("w:type"), "dxa")


def add_inline_markdown(paragraph, text, default_size=10.5, default_color=INK):
    parts = re.split(r"(\*\*.*?\*\*)", text)
    for part in parts:
        if not part:
            continue
        if part.startswith("**") and part.endswith("**"):
            run = paragraph.add_run(part[2:-2])
            set_run_font(run, default_size, True, NAVY)
        else:
            run = paragraph.add_run(part)
            set_run_font(run, default_size, False, default_color)


def define_styles(doc):
    styles = doc.styles

    normal = styles["Normal"]
    normal.font.name = FONT_LATIN
    normal._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), FONT_CN)
    normal.font.size = Pt(10.5)
    normal.font.color.rgb = RGBColor.from_string(INK)
    normal.paragraph_format.space_after = Pt(5)
    normal.paragraph_format.line_spacing = 1.35
    normal.paragraph_format.widow_control = True

    specs = {
        "KBT Document Title": (WD_STYLE_TYPE.PARAGRAPH, 22, True, NAVY, 0, 12),
        "KBT Lead": (WD_STYLE_TYPE.PARAGRAPH, 11, False, MUTED, 0, 8),
        "KBT Heading 1": (WD_STYLE_TYPE.PARAGRAPH, 16, True, NAVY, 14, 7),
        "KBT Heading 2": (WD_STYLE_TYPE.PARAGRAPH, 12.5, True, NAVY, 10, 5),
        "KBT Body": (WD_STYLE_TYPE.PARAGRAPH, 10.5, False, INK, 0, 5),
        "KBT Callout": (WD_STYLE_TYPE.PARAGRAPH, 10, False, INK, 6, 6),
    }
    for name, (style_type, size, bold, color, before, after) in specs.items():
        style = styles[name] if name in styles else styles.add_style(name, style_type)
        style.font.name = FONT_LATIN
        style._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), FONT_CN)
        style.font.size = Pt(size)
        style.font.bold = bold
        style.font.color.rgb = RGBColor.from_string(color)
        style.paragraph_format.space_before = Pt(before)
        style.paragraph_format.space_after = Pt(after)
        style.paragraph_format.line_spacing = 1.25 if size >= 12 else 1.35
        style.paragraph_format.widow_control = True
        if "Heading" in name:
            style.paragraph_format.keep_with_next = True

    list_style = styles["KBT List"] if "KBT List" in styles else styles.add_style("KBT List", WD_STYLE_TYPE.PARAGRAPH)
    list_style.font.name = FONT_LATIN
    list_style._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), FONT_CN)
    list_style.font.size = Pt(10.5)
    list_style.paragraph_format.space_after = Pt(3)
    list_style.paragraph_format.line_spacing = 1.3


def add_page_number_footer(doc):
    section = doc.sections[0]
    footer = section.footer
    p = footer.paragraphs[0]
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    p.clear()
    r = p.add_run("数科事业部八月周会会议纪要  ·  ")
    set_run_font(r, 8.5, False, MUTED)
    fld_char1 = OxmlElement("w:fldChar")
    fld_char1.set(qn("w:fldCharType"), "begin")
    instr_text = OxmlElement("w:instrText")
    instr_text.set(qn("xml:space"), "preserve")
    instr_text.text = " PAGE "
    fld_char2 = OxmlElement("w:fldChar")
    fld_char2.set(qn("w:fldCharType"), "end")
    r._r.append(fld_char1)
    r._r.append(instr_text)
    r._r.append(fld_char2)


def normalize_retained_header_font(doc):
    for section in doc.sections:
        for header in (section.header, section.even_page_header):
            for paragraph in header.paragraphs:
                for run in paragraph.runs:
                    if run.text:
                        r_fonts = run._element.get_or_add_rPr().get_or_add_rFonts()
                        r_fonts.set(qn("w:eastAsia"), FONT_CN)
                        r_fonts.set(qn("w:ascii"), FONT_CN)
                        r_fonts.set(qn("w:hAnsi"), FONT_CN)
            for doc_pr in header._element.iter(qn("wp:docPr")):
                doc_pr.set("title", "康比特品牌标识")
                doc_pr.set("descr", "康比特品牌标识")


def duplicate_default_header_to_even_pages(section):
    """Create a real even-page header part with the same content and assets."""
    source_header = section.header
    even_header = section.even_page_header
    even_header.is_linked_to_previous = False
    rel_map = {}
    for rel in source_header.part.rels.values():
        target = rel.target_ref if rel.is_external else rel.target_part
        rel_map[rel.rId] = even_header.part.relate_to(target, rel.reltype, rel.is_external)
    for child in list(even_header._element):
        even_header._element.remove(child)
    for child in source_header._element:
        cloned = deepcopy(child)
        for element in cloned.iter():
            for attr_name in (qn("r:id"), qn("r:embed"), qn("r:link")):
                old_rel_id = element.get(attr_name)
                if old_rel_id in rel_map:
                    element.set(attr_name, rel_map[old_rel_id])
        even_header._element.append(cloned)


def add_note_box(doc, text):
    table = doc.add_table(rows=1, cols=1)
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    set_table_geometry(table, [5.85])
    set_table_borders(table, color="E8C59F", size="5")
    cell = table.cell(0, 0)
    shade_cell(cell, PALE_ORANGE)
    set_cell_margins(cell, top=140, start=170, bottom=140, end=170)
    p = cell.paragraphs[0]
    p.style = "KBT Callout"
    p.paragraph_format.space_after = Pt(0)
    add_inline_markdown(p, text, default_size=9.5)
    spacer = doc.add_paragraph()
    spacer.paragraph_format.space_after = Pt(2)


def add_markdown_table(doc, rows):
    table = doc.add_table(rows=0, cols=len(rows[0]))
    for row_values in rows:
        cells = table.add_row().cells
        for idx, value in enumerate(row_values):
            cells[idx].text = ""
            p = cells[idx].paragraphs[0]
            p.paragraph_format.space_after = Pt(0)
            p.paragraph_format.line_spacing = 1.2
            add_inline_markdown(p, value, default_size=8.5 if len(rows[0]) >= 6 else 9)
            cells[idx].vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
            set_cell_margins(cells[idx])
    set_repeat_table_header(table.rows[0])
    for row in table.rows:
        prevent_row_split(row)
    for cell in table.rows[0].cells:
        shade_cell(cell, PALE_BLUE)
        for run in cell.paragraphs[0].runs:
            set_run_font(run, 9, True, NAVY)
        cell.paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
    for row in table.rows[1:]:
        row.cells[0].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
        if len(row.cells) >= 4:
            row.cells[3].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
        if len(row.cells) >= 6:
            row.cells[5].paragraphs[0].alignment = WD_ALIGN_PARAGRAPH.CENTER
    widths = [0.35, 1.25, 1.15, 0.72, 1.83, 0.55] if len(rows[0]) == 6 else [5.85 / len(rows[0])] * len(rows[0])
    set_table_geometry(table, widths)
    set_table_borders(table)
    table.rows[-1].cells[0].paragraphs[0].paragraph_format.space_after = Pt(0)
    doc.add_paragraph().paragraph_format.space_after = Pt(2)


def build(source_md: Path, starter_docx: Path, output_docx: Path):
    doc = Document(starter_docx)
    # LibreOffice respects the template's left/right page styles. Explicitly
    # map the same branded header to even pages so the identity is visible on
    # every page in both Word and PDF rendering.
    doc.settings.odd_and_even_pages_header_footer = True
    for existing_section in doc.sections:
        existing_section.different_first_page_header_footer = False
    define_styles(doc)
    bullet_num_id = ensure_numbering(doc, "bullet", "•")
    section = doc.sections[0]
    duplicate_default_header_to_even_pages(section)
    section.top_margin = Cm(2.54)
    section.bottom_margin = Cm(2.2)
    section.left_margin = Cm(3.175)
    section.right_margin = Cm(3.175)
    section.header_distance = Cm(1.2)
    section.footer_distance = Cm(1.0)

    body = doc._element.body
    for child in list(body):
        if child.tag != qn("w:sectPr"):
            body.remove(child)

    lines = source_md.read_text(encoding="utf-8").splitlines()
    i = 0
    in_ordered_list = False
    current_number_num_id = None
    while i < len(lines):
        line = lines[i].rstrip()
        if not line:
            in_ordered_list = False
            i += 1
            continue
        is_ordered_line = bool(re.match(r"^\d+\. ", line))
        if not is_ordered_line:
            in_ordered_list = False
        if line.startswith("# "):
            p = doc.add_paragraph(style="KBT Document Title")
            p.alignment = WD_ALIGN_PARAGRAPH.CENTER
            p.paragraph_format.space_before = Pt(18)
            add_inline_markdown(p, line[2:], default_size=22)
            for run in p.runs:
                set_run_font(run, 22, True, NAVY)
            sub = doc.add_paragraph(style="KBT Lead")
            sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
            add_inline_markdown(sub, "2026 年 8 月 17 日 · 华亭 11 层", default_size=11, default_color=MUTED)
        elif line.startswith("## "):
            title = line[3:]
            p = doc.add_paragraph(style="KBT Heading 1")
            add_inline_markdown(p, title, default_size=16)
            for run in p.runs:
                set_run_font(run, 16, True, NAVY)
        elif line.startswith("### "):
            p = doc.add_paragraph(style="KBT Heading 2")
            add_inline_markdown(p, line[4:], default_size=12.5)
            for run in p.runs:
                set_run_font(run, 12.5, True, NAVY)
        elif line.startswith("> "):
            add_note_box(doc, line[2:])
        elif line.startswith("- "):
            p = doc.add_paragraph(style="KBT List")
            apply_numbering(p, bullet_num_id)
            add_inline_markdown(p, line[2:])
        elif re.match(r"^\d+\. ", line):
            if not in_ordered_list:
                current_number_num_id = ensure_numbering(doc, "decimal", "%1.")
                in_ordered_list = True
            p = doc.add_paragraph(style="KBT List")
            apply_numbering(p, current_number_num_id)
            add_inline_markdown(p, re.sub(r"^\d+\. ", "", line))
        elif line.startswith("|") and i + 1 < len(lines) and re.match(r"^\|[\s:|-]+\|$", lines[i + 1]):
            table_rows = []
            header = [c.strip() for c in line.strip("|").split("|")]
            table_rows.append(header)
            i += 2
            while i < len(lines) and lines[i].startswith("|"):
                table_rows.append([c.strip() for c in lines[i].strip("|").split("|")])
                i += 1
            add_markdown_table(doc, table_rows)
            continue
        elif line.startswith("**") and line.endswith("**"):
            p = doc.add_paragraph(style="KBT Body")
            p.paragraph_format.space_before = Pt(5)
            add_inline_markdown(p, line, default_size=10.5)
        else:
            p = doc.add_paragraph(style="KBT Body")
            add_inline_markdown(p, line)
        i += 1

    add_page_number_footer(doc)
    normalize_retained_header_font(doc)
    output_docx.parent.mkdir(parents=True, exist_ok=True)
    doc.save(output_docx)


if __name__ == "__main__":
    if len(sys.argv) != 4:
        raise SystemExit("usage: build_minutes_docx.py SOURCE.md STARTER.docx OUTPUT.docx")
    build(Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]))
