import re
import sys
import tempfile
from pathlib import Path
from docx import Document
from docx.shared import Pt, Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
import asyncio
from .render_mermaid import render_fast as render_mermaid_png, close_fast as close_mermaid
import subprocess
import textwrap

def sanitize(text: str) -> str:
    return ''.join(ch for ch in text if ch >= ' ' or ch in ('\t'))

def add_para(doc, text, style=None):
    p = doc.add_paragraph()
    if style:
        p.style = style
    r = p.add_run(sanitize(text))
    return p, r

def add_bookmark(paragraph, name: str, bkm_id: int):
    start = OxmlElement('w:bookmarkStart')
    start.set(qn('w:id'), str(bkm_id))
    start.set(qn('w:name'), name)
    paragraph._p.insert(0, start)
    end = OxmlElement('w:bookmarkEnd')
    end.set(qn('w:id'), str(bkm_id))
    paragraph._p.append(end)

def add_internal_link(paragraph, text: str, anchor: str):
    hyperlink = OxmlElement('w:hyperlink')
    hyperlink.set(qn('w:anchor'), anchor)
    r = OxmlElement('w:r')
    rPr = OxmlElement('w:rPr')
    rStyle = OxmlElement('w:rStyle')
    rStyle.set(qn('w:val'), 'Hyperlink')
    rPr.append(rStyle)
    t = OxmlElement('w:t')
    t.text = sanitize(text)
    r.append(rPr)
    r.append(t)
    hyperlink.append(r)
    paragraph._p.append(hyperlink)

def _apply_profile(doc: Document, profile: str):
    p = (profile or '').lower()
    normal = doc.styles['Normal']
    pf = normal.paragraph_format
    if p == 'paper':
        normal.font.size = Pt(12)
        pf.first_line_indent = Inches(0.25)
        pf.space_after = Pt(6)
        pf.space_before = Pt(0)
        for k in range(1,7):
            try:
                s = doc.styles[f'Heading {k}']
                s.font.size = Pt(max(14, 20 - k*2))
            except Exception:
                pass
    elif p == 'patent':
        normal.font.size = Pt(12)
        pf.first_line_indent = Inches(0.3)
        pf.space_after = Pt(4)
        pf.space_before = Pt(0)
        for k in range(1,7):
            try:
                s = doc.styles[f'Heading {k}']
                s.font.size = Pt(max(13, 18 - k*2))
            except Exception:
                pass
    elif p in ('softcopy','soft','software'):
        normal.font.size = Pt(12)
        pf.first_line_indent = Inches(0.25)
        pf.space_after = Pt(8)
        pf.space_before = Pt(0)
        for k in range(1,7):
            try:
                s = doc.styles[f'Heading {k}']
                s.font.size = Pt(max(14, 22 - k*2))
            except Exception:
                pass

def parse(md_text: str, workdir: Path, profile: str = '', run_code: bool = True, render_mermaid: bool = True, quiet: bool = False, progress_cb=None) -> Document:
    doc = Document()
    section = doc.sections[0]
    avail = section.page_width - section.left_margin - section.right_margin
    avail_h = section.page_height - section.top_margin - section.bottom_margin
    normal = doc.styles['Normal']
    normal.font.size = Pt(11)
    pf = normal.paragraph_format
    pf.space_after = Pt(6)
    pf.space_before = Pt(0)
    _apply_profile(doc, profile)
    lines = md_text.splitlines()
    pre_heading_re = re.compile(r'^(#{1,6})\s+(.*)$')
    pre_anchor_re = re.compile(r'^<a\s+id\=\"([^\"]+)\"\s*>\s*</a>\s*$')
    pre_toclink_re = re.compile(r'^\s*[-*]\s*\[(.*?)\]\(#([^\)]+)\)\s*$')
    toc_items = []
    pending_anchor = None
    def strip_leading_number(t: str) -> str:
        return re.sub(r'^\s*\d+(?:\.\d+)*\s+', '', t)
    pre_counters = [0,0,0,0,0,0]
    pre_in_code = False
    toc_anchor_set = set()
    in_toc_scan = False
    j = 0
    while j < len(lines):
        ln = lines[j]
        s0 = ln.rstrip('\n')
        if s0.startswith('## 目录'):
            in_toc_scan = True
            j += 1
            continue
        if in_toc_scan:
            mt0 = pre_toclink_re.match(s0)
            if mt0:
                toc_anchor_set.add(mt0.group(2))
                j += 1
                continue
            else:
                in_toc_scan = False
        if s0.startswith('```'):
            pre_in_code = not pre_in_code
            j += 1
            continue
        if pre_in_code:
            j += 1
            continue
        ms = pre_anchor_re.match(s0)
        if ms:
            pending_anchor = ms.group(1)
            j += 1
            continue
        mh = pre_heading_re.match(s0)
        if mh:
            lvl = len(mh.group(1))
            txt = strip_leading_number(mh.group(2).strip())
            pre_counters[lvl-1] += 1
            for k in range(lvl, 6):
                pre_counters[k] = 0
            num = '.'.join(str(pre_counters[x]) for x in range(lvl) if pre_counters[x] > 0)
            toc_items.append((lvl, f"{num} {txt}", pending_anchor))
            pending_anchor = None
        j += 1
    in_code = False
    code_lang = ''
    buf = []
    heading_re = re.compile(r'^(#{1,6})\s+(.*)$')
    bullet_re = re.compile(r'^\s*[-*]\s+(.*)$')
    number_re = re.compile(r'^\s*\d+\.\s+(.*)$')
    enum_re = re.compile(r'^\s*\d+[)\.、:]\s+(.*)$')
    anchor_re = re.compile(r'^<a\s+id\=\"([^\"]+)\"\s*>\s*</a>\s*$')
    mdlink_re = re.compile(r'^\s*\[(.*?)\]\(#([^\)]+)\)\s*$')
    toclink_re = re.compile(r'^\s*[-*]\s*\[(.*?)\]\(#([^\)]+)\)\s*$')
    table_row_re = re.compile(r'^\s*\|.*\|\s*$')
    table_sep_re = re.compile(r'^\s*\|(?:\s*:?-{3,}:?\s*\|)+\s*$')
    mermaid_inline_start_re = re.compile(r'^(?:\s*%%\{.*?\}%%\s*)?(?:graph|flowchart|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|journey|pie|mindmap)\b')

    bkm_counter = 1
    in_toc = False
    in_html_comment = False
    prev_blank = False
    counters = [0,0,0,0,0,0]
    pending_anchor_name = None

    i = 0
    while i < len(lines):
        line = lines[i]
        s = line.rstrip('\n')
        s_norm = s.replace('\u00A0',' ').replace('\u200b','').replace('\ufeff','').replace('\u3000',' ')
        if not in_code:
            if '<!--' in s and '-->' not in s:
                in_html_comment = True
                i += 1
                continue
            if in_html_comment:
                if '-->' in s:
                    s = s.split('-->',1)[1].strip()
                    in_html_comment = False
                else:
                    i += 1
                    continue
            s = re.sub(r'<!--.*?-->', '', s)
            if re.match(r'^\s*-{3,}\s*$', s):
                i += 1
                continue
        if s_norm.startswith('```'):
            if not in_code:
                in_code = True
                code_lang = s_norm[3:].strip()
                buf = []
            else:
                if code_lang.lower() == 'mermaid':
                    mmd_path = workdir / f'mmd_{i}.mmd'
                    png_path = workdir / f'mmd_{i}.png'
                    mmd_path.write_text('\n'.join(buf), encoding='utf-8')
                    try:
                        if not render_mermaid:
                            raise BaseException('mermaid rendering disabled')
                        render_mermaid_png('\n'.join(buf), png_path)
                        def _png_size(p):
                            import struct
                            with open(p, 'rb') as f:
                                sig = f.read(8)
                                if sig != b'\x89PNG\r\n\x1a\n':
                                    return None
                                while True:
                                    data = f.read(8)
                                    if not data:
                                        return None
                                    clen, ctype = struct.unpack('>I4s', data)
                                    if ctype == b'IHDR':
                                        wh = f.read(8)
                                        w, h = struct.unpack('>II', wh)
                                        return w, h
                                    f.seek(clen + 4, 1)
                        sz = _png_size(str(png_path))
                        pic = doc.add_picture(str(png_path))
                        if sz:
                            w_px, h_px = sz
                            max_w = avail
                            max_h = int(avail_h * 3 / 5)
                            if w_px == 0 or h_px == 0:
                                scaled_w, scaled_h = max_w, int(max_h)
                            else:
                                ratio = w_px / h_px
                                scaled_w = max_w
                                scaled_h = int(max_w / ratio)
                                if scaled_h > max_h:
                                    scaled_h = int(max_h)
                                    scaled_w = int(max_h * ratio)
                            pic.width = scaled_w
                            pic.height = scaled_h
                        if doc.paragraphs:
                            doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
                    except BaseException:
                        add_para(doc, '[mermaid]', None)
                        for l in buf:
                            p, r = add_para(doc, l, None)
                            r.font.name = 'Consolas'
                            r.font.size = Pt(10)
                else:
                    code_text = '\n'.join(buf)
                    code_text_clean = (
                        code_text.replace('\u00A0',' ').replace('\u200b','').replace('\ufeff','').replace('\u3000',' ')
                    )
                    is_matplotlib = (
                        'import matplotlib' in code_text
                        or 'from matplotlib' in code_text
                    )
                    if is_matplotlib and run_code:
                        png_path = workdir / f'pyplot_{i}.png'
                        ok = False
                        try:
                            runner = workdir / f'run_plot_{i}.py'
                            script_lines = [
                                "import os",
                                "os.environ['MPLBACKEND'] = 'Agg'",
                                "import matplotlib",
                                "matplotlib.use('Agg')",
                                "import matplotlib.pyplot as plt",
                                "from matplotlib import font_manager",
                                "_cjk_candidates = ['Microsoft YaHei','SimHei','Noto Sans CJK SC','Source Han Sans SC','Arial Unicode MS']",
                                "for _name in _cjk_candidates:",
                                "    try:",
                                "        _path = font_manager.findfont(_name, fallback_to_default=False)",
                                "        if _path:",
                                "            matplotlib.rcParams['font.sans-serif'] = [_name]",
                                "            matplotlib.rcParams['axes.unicode_minus'] = False",
                                "            break",
                                "    except Exception:",
                                "        pass",
                                "",
                                "# === user code start ===",
                                code_text_clean,
                                "# === user code end ===",
                                "try:",
                                "    figs = [plt.figure(n) for n in plt.get_fignums()]",
                                "    if not figs:",
                                "        figs = [plt.gcf()]",
                                f"    figs[-1].savefig(r\"{str(png_path)}\", dpi=150, bbox_inches='tight')",
                                "except Exception as _e:",
                                "    raise",
                                "finally:",
                                "    plt.close('all')",
                            ]
                            runner.write_text("\n".join(script_lines), encoding='utf-8')
                            if quiet:
                                subprocess.run([sys.executable, str(runner)], check=True, timeout=60, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                            else:
                                subprocess.run([sys.executable, str(runner)], check=True, timeout=60)
                            ok = png_path.exists()
                        except BaseException:
                            ok = False
                        if ok:
                            def _png_size(p):
                                import struct
                                with open(p, 'rb') as f:
                                    sig = f.read(8)
                                    if sig != b'\x89PNG\r\n\x1a\n':
                                        return None
                                    while True:
                                        data = f.read(8)
                                        if not data:
                                            return None
                                        clen, ctype = struct.unpack('>I4s', data)
                                        if ctype == b'IHDR':
                                            wh = f.read(8)
                                            w, h = struct.unpack('>II', wh)
                                            return w, h
                                        f.seek(clen + 4, 1)
                            sz = _png_size(str(png_path))
                            pic = doc.add_picture(str(png_path))
                            if sz:
                                w_px, h_px = sz
                                max_w = avail
                                max_h = int(avail_h * 3 / 5)
                                if w_px == 0 or h_px == 0:
                                    scaled_w, scaled_h = max_w, int(max_h)
                                else:
                                    ratio = w_px / h_px
                                    scaled_w = max_w
                                    scaled_h = int(max_w / ratio)
                                    if scaled_h > max_h:
                                        scaled_h = int(max_h)
                                        scaled_w = int(max_h * ratio)
                                pic.width = scaled_w
                                pic.height = scaled_h
                            if doc.paragraphs:
                                doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
                        else:
                            add_para(doc, f'[{code_lang or "code"}]', None)
                            for l in buf:
                                p, r = add_para(doc, l, None)
                                r.font.name = 'Consolas'
                                r.font.size = Pt(10)
                    else:
                        add_para(doc, f'[{code_lang or "code"}]', None)
                        for l in buf:
                            p, r = add_para(doc, l, None)
                            r.font.name = 'Consolas'
                            r.font.size = Pt(10)
                in_code = False
                code_lang = ''
                buf = []
            i += 1
            try:
                if progress_cb:
                    progress_cb(min(99, int(i * 100 / max(1, len(lines)))))
            except Exception:
                pass
            continue
        if in_code:
            buf.append(s_norm)
            i += 1
            try:
                if progress_cb:
                    progress_cb(min(99, int(i * 100 / max(1, len(lines)))))
            except Exception:
                pass
            continue

        if s_norm.startswith('## 目录'):
            p, r = add_para(doc, '目录', 'Heading 2')
            r.font.size = Pt(20)
            p.alignment = WD_ALIGN_PARAGRAPH.CENTER
            in_toc = True
            i += 1
            try:
                if progress_cb:
                    progress_cb(min(99, int(i * 100 / max(1, len(lines)))))
            except Exception:
                pass
            continue

        if in_toc:
            if s.strip() == '':
                doc.add_page_break()
                in_toc = False
                i += 1
                continue
            mt = toclink_re.match(s)
            if mt:
                txt, anc = mt.group(1), mt.group(2)
                mlead = re.match(r'^(\s*)[-*]', s)
                indent_spaces = len(mlead.group(1)) if mlead else 0
                indent_level = max(0, indent_spaces // 2)
                tp = doc.add_paragraph()
                tp.paragraph_format.left_indent = Inches(0.25) * indent_level
                add_internal_link(tp, txt, anc)
                i += 1
                continue
            doc.add_page_break()
            in_toc = False

        # Marker-based inline mermaid: a line with [mermaid] followed by diagram lines
        if not in_code and s_norm.strip().lower() == '[mermaid]':
            buf_inline = []
            i += 1
            while i < len(lines):
                ns = lines[i].rstrip('\n')
                ns_norm = ns.replace('\u00A0',' ').replace('\u200b','').replace('\ufeff','').replace('\u3000',' ')
                if anchor_re.match(ns_norm) or heading_re.match(ns_norm) or ns_norm.strip() == '' or ns_norm.startswith('```') or table_row_re.match(ns_norm):
                    break
                buf_inline.append(ns_norm)
                i += 1
            mmd_path = workdir / f'mmd_inline_{i}.mmd'
            png_path = workdir / f'mmd_inline_{i}.png'
            mmd_path.write_text('\n'.join(buf_inline), encoding='utf-8')
            try:
                if not render_mermaid:
                    raise BaseException('mermaid rendering disabled')
                render_mermaid_png('\n'.join(buf_inline), png_path)
                def _png_size(p):
                    import struct
                    with open(p, 'rb') as f:
                        sig = f.read(8)
                        if sig != b'\x89PNG\r\n\x1a\n':
                            return None
                        while True:
                            data = f.read(8)
                            if not data:
                                return None
                            clen, ctype = struct.unpack('>I4s', data)
                            if ctype == b'IHDR':
                                wh = f.read(8)
                                w, h = struct.unpack('>II', wh)
                                return w, h
                            f.seek(clen + 4, 1)
                sz = _png_size(str(png_path))
                pic = doc.add_picture(str(png_path))
                if sz:
                    w_px, h_px = sz
                    max_w = avail
                    max_h = int(avail_h * 3 / 5)
                    if w_px == 0 or h_px == 0:
                        scaled_w, scaled_h = max_w, int(max_h)
                    else:
                        ratio = w_px / h_px
                        scaled_w = max_w
                        scaled_h = int(max_w / ratio)
                        if scaled_h > max_h:
                            scaled_h = int(max_h)
                            scaled_w = int(max_h * ratio)
                    pic.width = scaled_w
                    pic.height = scaled_h
                if doc.paragraphs:
                    doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
            except BaseException:
                add_para(doc, '[mermaid]', None)
                for l in buf_inline:
                    p, r = add_para(doc, l, None)
                    r.font.name = 'Consolas'
                    r.font.size = Pt(10)
            i += 1
            try:
                if progress_cb:
                    progress_cb(min(99, int(i * 100 / max(1, len(lines)))))
            except Exception:
                pass
            continue

        if not in_code and mermaid_inline_start_re.match(s_norm):
            buf_inline = [s]
            i += 1
            while i < len(lines):
                ns = lines[i].rstrip('\n')
                ns_norm = ns.replace('\u00A0',' ').replace('\u200b','').replace('\ufeff','').replace('\u3000',' ')
                if anchor_re.match(ns_norm) or heading_re.match(ns_norm) or ns_norm.startswith('```') or table_row_re.match(ns_norm):
                    break
                buf_inline.append(ns_norm)
                i += 1
            mmd_path = workdir / f'mmd_inline_{i}.mmd'
            png_path = workdir / f'mmd_inline_{i}.png'
            mmd_path.write_text('\n'.join(buf_inline), encoding='utf-8')
            try:
                if not render_mermaid:
                    raise BaseException('mermaid rendering disabled')
                asyncio.run(render_mermaid_png('\n'.join(buf_inline), png_path))
                def _png_size(p):
                    import struct
                    with open(p, 'rb') as f:
                        sig = f.read(8)
                        if sig != b'\x89PNG\r\n\x1a\n':
                            return None
                        while True:
                            data = f.read(8)
                            if not data:
                                return None
                            clen, ctype = struct.unpack('>I4s', data)
                            if ctype == b'IHDR':
                                wh = f.read(8)
                                w, h = struct.unpack('>II', wh)
                                return w, h
                            f.seek(clen + 4, 1)
                sz = _png_size(str(png_path))
                pic = doc.add_picture(str(png_path))
                if sz:
                    w_px, h_px = sz
                    max_w = avail
                    max_h = int(avail_h * 3 / 5)
                    if w_px == 0 or h_px == 0:
                        scaled_w, scaled_h = max_w, int(max_h)
                    else:
                        ratio = w_px / h_px
                        scaled_w = max_w
                        scaled_h = int(max_w / ratio)
                        if scaled_h > max_h:
                            scaled_h = int(max_h)
                            scaled_w = int(max_h * ratio)
                    pic.width = scaled_w
                    pic.height = scaled_h
                if doc.paragraphs:
                    doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
            except BaseException:
                for l in buf_inline:
                    p, r = add_para(doc, l, None)
                    r.font.name = 'Consolas'
                    r.font.size = Pt(10)
            continue

        if table_row_re.match(s) and (i + 1) < len(lines) and table_sep_re.match(lines[i+1].rstrip('\n')):
            tbl_lines = [s]
            i += 1
            tbl_sep = lines[i].rstrip('\n')
            i += 1
            while i < len(lines):
                ns = lines[i].rstrip('\n')
                if table_row_re.match(ns):
                    tbl_lines.append(ns)
                    i += 1
                else:
                    break

            def parse_row(row):
                parts = [c.strip() for c in row.split('|')]
                if parts and parts[0] == '':
                    parts = parts[1:]
                if parts and parts[-1] == '':
                    parts = parts[:-1]
                return parts

            header = parse_row(tbl_lines[0])
            sep = parse_row(tbl_sep)
            body_rows = [parse_row(r) for r in tbl_lines[1:]]

            png_path = workdir / f'table_{i}.png'
            ok_tbl = False
            try:
                runner = workdir / f'run_table_{i}.py'
                import json
                js_header = json.dumps(header, ensure_ascii=False)
                js_rows = json.dumps(body_rows, ensure_ascii=False)
                script_lines = [
                    "import os",
                    "os.environ['MPLBACKEND'] = 'Agg'",
                    "import matplotlib",
                    "matplotlib.use('Agg')",
                    "import matplotlib.pyplot as plt",
                    "from matplotlib import font_manager",
                    "from matplotlib.font_manager import FontProperties",
                    "_cjk_candidates = ['Microsoft YaHei','SimHei','Noto Sans CJK SC','Source Han Sans SC','Arial Unicode MS']",
                    "for _name in _cjk_candidates:",
                    "    try:",
                    "        _path = font_manager.findfont(_name, fallback_to_default=False)",
                    "        if _path:",
                    "            matplotlib.rcParams['font.sans-serif'] = [_name]",
                    "            matplotlib.rcParams['axes.unicode_minus'] = False",
                    "            break",
                    "    except Exception:",
                    "        pass",
                    f"headers = {js_header}",
                    f"rows = {js_rows}",
                    "fig, ax = plt.subplots(figsize=(max(6, len(headers)*1.2), max(2, len(rows)*0.7)))",
                    "ax.axis('off')",
                    "tbl = ax.table(cellText=rows, colLabels=headers, loc='center')",
                    "tbl.auto_set_font_size(False)",
                    "tbl.set_fontsize(9)",
                    "_fam = matplotlib.rcParams.get('font.sans-serif',[None])[0]",
                    "_fp = FontProperties(family=_fam, size=9) if _fam else None",
                    "if _fp is not None:",
                    "    for _c in tbl.get_celld().values():",
                    "        _c.get_text().set_fontproperties(_fp)",
                    "tbl.scale(1, 1.25)",
                    "fig.tight_layout()",
                    f"fig.savefig(r\"{str(png_path)}\", dpi=180, bbox_inches='tight')",
                    "plt.close(fig)",
                ]
                runner.write_text("\n".join(script_lines), encoding='utf-8')
                if run_code:
                    if quiet:
                        subprocess.run([sys.executable, str(runner)], check=True, timeout=60, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    else:
                        subprocess.run([sys.executable, str(runner)], check=True, timeout=60)
                ok_tbl = png_path.exists()
            except Exception:
                ok_tbl = False

            if ok_tbl:
                def _png_size(p):
                    import struct
                    with open(p, 'rb') as f:
                        sig = f.read(8)
                        if sig != b'\x89PNG\r\n\x1a\n':
                            return None
                        while True:
                            data = f.read(8)
                            if not data:
                                return None
                            clen, ctype = struct.unpack('>I4s', data)
                            if ctype == b'IHDR':
                                wh = f.read(8)
                                w, h = struct.unpack('>II', wh)
                                return w, h
                            f.seek(clen + 4, 1)
                sz = _png_size(str(png_path))
                pic = doc.add_picture(str(png_path))
                if sz:
                    w_px, h_px = sz
                    max_w = avail
                    max_h = int(avail_h * 3 / 5)
                    if w_px == 0 or h_px == 0:
                        scaled_w, scaled_h = max_w, int(max_h)
                    else:
                        ratio = w_px / h_px
                        scaled_w = max_w
                        scaled_h = int(max_w / ratio)
                        if scaled_h > max_h:
                            scaled_h = int(max_h)
                            scaled_w = int(max_h * ratio)
                    pic.width = scaled_w
                    pic.height = scaled_h
                if doc.paragraphs:
                    doc.paragraphs[-1].alignment = WD_ALIGN_PARAGRAPH.CENTER
                continue
            else:
                aligns = []
                for p in sep:
                    ps = p.strip()
                    left = ps.startswith(':')
                    right = ps.endswith(':')
                    if left and right:
                        aligns.append(WD_ALIGN_PARAGRAPH.CENTER)
                    elif right:
                        aligns.append(WD_ALIGN_PARAGRAPH.RIGHT)
                    else:
                        aligns.append(WD_ALIGN_PARAGRAPH.LEFT)
                cols = len(header)
                rows = len(body_rows) + 1
                table = doc.add_table(rows=rows, cols=cols)
                table.style = 'Table Grid'
                for j, h in enumerate(header):
                    cell = table.cell(0, j)
                    cell.text = ''
                    para = cell.paragraphs[0]
                    para.alignment = aligns[j] if j < len(aligns) else WD_ALIGN_PARAGRAPH.LEFT
                    run = para.add_run(sanitize(h))
                    run.bold = True
                for ri, row_vals in enumerate(body_rows):
                    for cj in range(cols):
                        val = row_vals[cj] if cj < len(row_vals) else ''
                        cell = table.cell(ri + 1, cj)
                        cell.text = ''
                        para = cell.paragraphs[0]
                        para.alignment = aligns[cj] if cj < len(aligns) else WD_ALIGN_PARAGRAPH.LEFT
                        para.add_run(sanitize(val))
                continue

        ma = anchor_re.match(s)
        if ma:
            name = ma.group(1)
            pending_anchor_name = name if name in toc_anchor_set else None
            i += 1
            try:
                if progress_cb:
                    progress_cb(min(99, int(i * 100 / max(1, len(lines)))))
            except Exception:
                pass
            continue

        ml = mdlink_re.match(s)
        if ml:
            text = ml.group(1)
            anchor = ml.group(2)
            p = doc.add_paragraph()
            add_internal_link(p, text, anchor)
            i += 1
            continue

        m = heading_re.match(s)
        if m:
            lvl = len(m.group(1))
            text = m.group(2).strip()
            style = f'Heading {min(6,lvl)}'
            p, r = add_para(doc, text, style)
            if pending_anchor_name:
                add_bookmark(p, pending_anchor_name, bkm_counter)
                bkm_counter += 1
                pending_anchor_name = None
            i += 1
            continue
        mb = bullet_re.match(s)
        if mb:
            add_para(doc, mb.group(1), None)
            i += 1
            continue
        mn = number_re.match(s)
        if mn:
            add_para(doc, mn.group(1), None)
            i += 1
            continue
        me = enum_re.match(s)
        if me:
            add_para(doc, me.group(1), None)
            i += 1
            continue
        if s.strip() == '':
            if not prev_blank:
                doc.add_paragraph('')
                prev_blank = True
            i += 1
            continue
        else:
            prev_blank = False
        add_para(doc, s, None)
        i += 1
    return doc

def main():
    import argparse
    import warnings
    warnings.filterwarnings("ignore", category=ResourceWarning)
    parser = argparse.ArgumentParser(
        prog='HOS_M2F.2docx',
        description='Markdown 转 DOCX，支持 Mermaid 渲染与 Matplotlib/表格转换为图片',
        epilog=(
            '示例:\n'
            '  HOS_M2F.2docx input.md output.docx [-p paper|patent|softcopy] [-nm] [-nc] [-q]\n'
            '相关命令:\n'
            '  HOS_M2F.2html input.md output.html [-p wx|csdn]\n'
            '  HOS_M2F.2pdf  input.md output.pdf\n'
            '  HOS_M2F.2json input.md output.json\n'
            '  HOS_M2F.2xml  input.md output.xml\n'
            '  HOS_M2F.2png  input.mmd output.png\n'
        ),
        formatter_class=argparse.RawTextHelpFormatter,
    )
    parser.add_argument('input', help='输入 Markdown 文件路径')
    parser.add_argument('output', help='输出 DOCX 文件路径')
    parser.add_argument('-p','--profile', choices=['paper','patent','softcopy'], default='', help='DOCX 段落/标题样式配置')
    parser.add_argument('-nm','--no-mermaid', action='store_true', help='禁用 Mermaid 渲染，按文本插入')
    parser.add_argument('-nc','--no-run-code', action='store_true', help='禁用代码执行（Matplotlib/表格渲染），按文本或表格插入')
    parser.add_argument('-q','--quiet', action='store_true', help='静默子进程输出')
    args = parser.parse_args()

    inp = Path(args.input)
    outp = Path(args.output)
    text = inp.read_text(encoding='utf-8')
    with tempfile.TemporaryDirectory() as td:
        doc = parse(
            text,
            Path(td),
            profile=args.profile,
            run_code=(not args.no_run_code),
            render_mermaid=(not args.no_mermaid),
            quiet=args.quiet,
        )
        doc.save(str(outp))
    try:
        close_mermaid()
    except Exception:
        pass
    print(f'Converted with mermaid: {inp} -> {outp}')

if __name__ == '__main__':
    main()
