"""
Word 文档导出模块
"""
import os
import re
import sqlite3
import logging
import copy
from time import perf_counter
from datetime import datetime
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

import config
from utils.outline_numbering import format_heading_display
from utils.style_manager import get_preset, docx_to_html_spec
from modules.dark_bid_format_check import check_technical_bid
from utils import attachment_section as att_sec

logger = logging.getLogger(__name__)

LEVEL_STYLES = {
    1: ('Heading 1', 16, True),
    2: ('Heading 2', 14, True),
    3: ('Heading 3', 13, False),
    4: ('Heading 4', 12, False),
}

_HF_TOKEN_RE = re.compile(r'(\{page\}|\{total\})', re.IGNORECASE)
_HEADER_BRAND_RE = re.compile(r'标桥\s*AI\s*编标', re.IGNORECASE)


def export_to_word(db_path: str, project_id: int, style_preset_name='standard') -> str:
    """
    生成 Word 文档并保存到 data/exports/，返回文件名。
    支持从首页「文件样式设置」传入预设 (style_preset_name)。
    """
    conn = sqlite3.connect(db_path)
    try:
        # 获取项目信息
        cur = conn.cursor()
        cur.execute(
            "SELECT name, COALESCE(enable_figure, 0), COALESCE(enable_table, 0) "
            "FROM projects WHERE id=?",
            (project_id,)
        )
        project = cur.fetchone()
        if not project:
            raise ValueError(f'项目 {project_id} 不存在')
        project_name = project[0]
        project_enable_figure = bool(project[1])
        project_enable_table = bool(project[2])

        # 获取标书大纲文本（用于标题页）
        cur.execute("SELECT outline FROM tender_data WHERE project_id=?", (project_id,))
        td = cur.fetchone()
        bid_title = project_name + '技术标书'
        if td and td[0]:
            first_line = td[0].strip().split('\n')[0].strip()
            if first_line:
                bid_title = first_line

        # 获取所有章节（按顺序）
        cur.execute('''
            SELECT section_number, section_title, level, is_leaf, content, intro_content
            FROM bid_sections
            WHERE project_id=?
            ORDER BY order_index
        ''', (project_id,))
        sections = cur.fetchall()

        t0 = perf_counter()
        raw_preset = get_preset(style_preset_name)
        preset = _enforce_format_constraints(raw_preset)

        # 导出时以“项目设置”中的图示/表格开关为准（运行时实时检查并自动应用）
        preset['figure_enabled'] = project_enable_figure
        preset['table_enabled'] = project_enable_table

        # 导出闭环：先做 Docx<->HTML 规格检查，不通过则自动修正后再出 Docx
        check_result = _run_docx_html_roundtrip_check(bid_title, sections, preset)
        if not check_result.get('overall', True):
            preset = _repair_preset_from_check(preset, check_result)
            # 修正后再次强制回灌项目开关，避免被修正逻辑覆盖
            preset['figure_enabled'] = project_enable_figure
            preset['table_enabled'] = project_enable_table
            check_result = _run_docx_html_roundtrip_check(bid_title, sections, preset)

        doc = _build_document(bid_title, sections, preset)

        # 保存文件
        os.makedirs(config.EXPORT_DIR, exist_ok=True)
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        safe_name = ''.join(c for c in project_name if c.isalnum() or c in '._- \u4e00-\u9fff')
        filename = f'{safe_name}_{timestamp}.docx'
        filepath = os.path.join(config.EXPORT_DIR, filename)
        doc.save(filepath)
        elapsed = perf_counter() - t0
        logger.info(
            f'导出完成: {filepath} (预设: {style_preset_name}, '
            f'图示: {"开" if project_enable_figure else "关"}, 表格: {"开" if project_enable_table else "关"}, '
            f'检查通过: {check_result.get("overall", True)}, 耗时: {elapsed:.2f}s)'
        )
        return filename

    finally:
        conn.close()


DISCLAIMER_TEXT = """\
免责声明

本工具仅供学习交流免费使用，所生成的技术方案不可直接用于投标，请务必人工核对。本工具不会通过任何平台进行销售，请用户注意辨别真伪。在您开始使用本AI标书制作服务之前，请认真阅读并同意以下关键条款。一旦您继续使用，即表示您已充分理解并认可本提示的全部内容。

服务定位
本工具为单机使用的AI标书辅助工具，旨在帮助您生成标书的参考素材。您需对最终自己编写的标书文件承担全部责任，包括审核、修改内容，确保其符合相关法律法规及项目要求。

准确性免责
本人不对AI生成内容的完全准确性与完整性作任何保证。您有义务自行核实所有关键信息，并自行承担因使用本工具所引发的一切后果。

标书风险
本工具所生成的素材文件仅作参考。若您使用（包括引用、修改或二次创作），需自行承担由此可能导致的废标、侵权等全部风险与责任，本人不承担任何相关责任。

责任限制
任何情形下，本人均不对因使用本服务而造成的任何直接、间接或衍生损失（例如利润损失、业务中断、数据丢失等）承担法律责任。

其他事项
本人保留随时修改或终止本服务的权利。本提示的解释及争议解决，均适用中华人民共和国法律。\
"""


def _add_disclaimer_page(doc: Document) -> None:
    """在文档开头插入免责声明页"""
    # 标题
    title_para = doc.add_paragraph()
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title_run = title_para.add_run('免责声明')
    title_run.font.size = Pt(16)
    title_run.font.bold = True
    title_run.font.name = '黑体'
    title_run._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')

    doc.add_paragraph()

    # 正文各段（跳过第一行标题，已单独渲染）
    body_lines = DISCLAIMER_TEXT.split('\n')[2:]  # 跳过"免责声明"和空行
    for line in body_lines:
        p = doc.add_paragraph()
        stripped = line.strip()
        # 小标题行（非空且后面没有缩进，即段落标题）
        is_section_title = bool(stripped) and not line.startswith(' ') and not line.startswith('\u3000')
        run = p.add_run(stripped if stripped else '')
        if is_section_title and stripped:
            run.font.bold = True
            run.font.size = Pt(11)
        else:
            run.font.size = Pt(10.5)
        run.font.name = 'Times New Roman'
        run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')
        p.paragraph_format.space_after = Pt(4)
        _set_line_spacing_15(p)

    doc.add_page_break()


def _add_toc_tree_page(doc: Document, sections: list) -> None:
    """标题页之后插入树状目录（按 level 缩进；静态文本，不含 Word 目录域）。"""
    toc_heading = doc.add_paragraph()
    toc_heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
    tr = toc_heading.add_run('目录')
    tr.font.size = Pt(16)
    tr.font.bold = True
    tr.font.name = '黑体'
    tr._element.rPr.rFonts.set(qn('w:eastAsia'), '黑体')
    doc.add_paragraph()

    for row in sections:
        section_number, title, level, _, _, _ = row
        level = min(int(level), 4)
        text = format_heading_display(level, str(section_number or ''), str(title or ''))
        p = doc.add_paragraph()
        p.paragraph_format.left_indent = Cm(0.75 * max(0, level - 1))
        p.paragraph_format.space_after = Pt(3)
        run = p.add_run(text)
        run.font.size = Pt(12)
        run.font.name = '宋体'
        run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')

    doc.add_page_break()


def _add_word_field(paragraph, instruction: str):
    """在段落中插入 Word 域（如 PAGE / NUMPAGES）。"""
    run = paragraph.add_run()
    fld_begin = OxmlElement('w:fldChar')
    fld_begin.set(qn('w:fldCharType'), 'begin')
    run._r.append(fld_begin)

    run = paragraph.add_run()
    instr = OxmlElement('w:instrText')
    instr.set(qn('xml:space'), 'preserve')
    instr.text = instruction
    run._r.append(instr)

    run = paragraph.add_run()
    fld_sep = OxmlElement('w:fldChar')
    fld_sep.set(qn('w:fldCharType'), 'separate')
    run._r.append(fld_sep)

    # 该文本必须位于 field begin/separate/end 之间，作为域初始显示值
    text_run = paragraph.add_run("1")
    text_run.font.name = 'Times New Roman'
    _safe_set_eastasia(text_run, '宋体')

    run = paragraph.add_run()
    fld_end = OxmlElement('w:fldChar')
    fld_end.set(qn('w:fldCharType'), 'end')
    run._r.append(fld_end)


def _apply_header_footer_template(paragraph, template: str):
    """将 {page}/{total} 模板渲染为 Word 可计算域。"""
    paragraph.clear()
    parts = _HF_TOKEN_RE.split(template or '')
    if not parts:
        parts = [template or '']
    for part in parts:
        token = (part or '').lower()
        if token == '{page}':
            _add_word_field(paragraph, 'PAGE')
        elif token == '{total}':
            _add_word_field(paragraph, 'NUMPAGES')
        elif part:
            run = paragraph.add_run(part)
            run.font.name = 'Times New Roman'
            _safe_set_eastasia(run, '宋体')
    paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER


def _sanitize_header_text(text: str) -> str:
    """清理页眉中的品牌默认文案，避免导出出现固定字符。"""
    cleaned = _HEADER_BRAND_RE.sub('', text or '')
    return cleaned.strip()


def _build_document(bid_title: str, sections, preset=None) -> Document:
    if preset is None:
        preset = get_preset('standard')

    doc = Document()

    # ── 页面设置 (from preset, overrides hardcoded values) ─────────────────────
    section_obj = doc.sections[0]
    m = preset.get('margins_cm', {'top': 2.5, 'bottom': 2.5, 'left': 3.0, 'right': 2.5})
    section_obj.page_width = Cm(21)
    section_obj.page_height = Cm(29.7)
    section_obj.left_margin = Cm(m.get('left', 3.0))
    section_obj.right_margin = Cm(m.get('right', 2.5))
    section_obj.top_margin = Cm(m.get('top', 2.5))
    section_obj.bottom_margin = Cm(m.get('bottom', 2.5))

    # Header / Footer from preset
    header_text = _sanitize_header_text((preset.get('header_text') or '').strip())
    footer_text = (preset.get('footer_text') or '').strip()

    # 页眉默认留空；页脚仍保留分页模板
    if not footer_text:
        footer_text = '第 {page} 页 / 共 {total} 页'

    header = section_obj.header
    if header.paragraphs:
        if header_text:
            _apply_header_footer_template(header.paragraphs[0], header_text)
        else:
            header.paragraphs[0].clear()

    footer = section_obj.footer
    if footer.paragraphs:
        _apply_header_footer_template(footer.paragraphs[0], footer_text)

    # ── 免责声明页（第一页）─────────────────────────────────────────────
    _add_disclaimer_page(doc)

    # ── 标题页 ──────────────────────────────────────────────────────────
    title_para = doc.add_paragraph()
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title_run = title_para.add_run(bid_title)
    title_run.font.size = Pt(22)
    title_run.font.bold = True
    title_run.font.color.rgb = RGBColor(0x1a, 0x56, 0xdb)
    title_run.font.name = preset.get('heading_font', '黑体')
    title_run._element.rPr.rFonts.set(qn('w:eastAsia'), preset.get('heading_font', '黑体'))

    doc.add_paragraph()

    date_para = doc.add_paragraph()
    date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
    date_run = date_para.add_run(datetime.now().strftime('%Y年%m月'))
    date_run.font.size = Pt(14)
    date_run.font.name = '宋体'
    date_run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')

    doc.add_page_break()

    # ── 树状目录页（标题页后、正文前）──────────────────────────────────
    _add_toc_tree_page(doc, sections)

    # ── 章节内容 ─────────────────────────────────────────────────────────
    for row in sections:
        section_number, title, level, is_leaf, content, intro = row
        level = min(int(level), 4)

        # 添加标题（带完整目录号，使用preset字体）
        heading_text = format_heading_display(level, str(section_number or ''), str(title or ''))
        heading = doc.add_heading(level=level)
        heading.clear()
        run = heading.add_run(heading_text)
        _, font_size, bold = LEVEL_STYLES.get(level, ('Heading 4', 12, False))
        run.font.size = Pt(preset.get(f'heading{level}_size_pt', font_size))
        run.font.bold = bold
        run.font.name = preset.get('heading_font', '黑体' if level <= 2 else '宋体')
        run._element.rPr.rFonts.set(qn('w:eastAsia'), preset.get('heading_font', '黑体' if level <= 2 else '宋体'))

        attachment_only = bool(att_sec.is_attachment_only_section(str(title or ''), att_sec.get_attachment_rules_cached()))

        # 章节引言（非叶节点）
        if intro and intro.strip():
            _add_body_paragraphs(doc, intro, preset, attachment_only=attachment_only, section_title=str(title or ''))

        # 正文内容（叶节点）
        if content and content.strip():
            _add_body_paragraphs(doc, content, preset, attachment_only=attachment_only, section_title=str(title or ''))

    return doc


def _set_line_spacing_15(paragraph):
    """将段落设为 1.5 倍行距（Word 中的 WD_LINE_SPACING.MULTIPLE × 1.5）"""
    from docx.oxml.ns import qn as _qn
    pPr = paragraph._element.get_or_add_pPr()
    spacing = pPr.find(_qn('w:spacing'))
    if spacing is None:
        spacing = OxmlElement('w:spacing')
        pPr.append(spacing)
    spacing.set(_qn('w:line'), '360')       # 240 × 1.5 = 360 twips
    spacing.set(_qn('w:lineRule'), 'auto')


def _safe_float(val, default):
    try:
        return float(val)
    except (TypeError, ValueError):
        return default


def _safe_int(val, default):
    try:
        return int(val)
    except (TypeError, ValueError):
        return default


def _enforce_format_constraints(preset: dict) -> dict:
    """格式限定：对关键排版参数做约束，避免导出失控。"""
    p = copy.deepcopy(preset or {})
    margins = p.get('margins_cm') or {}
    top = _safe_float(margins.get('top', 2.54), 2.54)
    bottom = _safe_float(margins.get('bottom', 2.54), 2.54)
    left = _safe_float(margins.get('left', 3.18), 3.18)
    right = _safe_float(margins.get('right', 3.18), 3.18)
    p['margins_cm'] = {
        'top': min(max(top, 1.5), 5.0),
        'bottom': min(max(bottom, 1.5), 5.0),
        'left': min(max(left, 2.0), 5.0),
        'right': min(max(right, 2.0), 5.0),
    }
    p['heading_font'] = p.get('heading_font') or '黑体'
    p['body_font'] = p.get('body_font') or '宋体'
    p['heading1_size_pt'] = _safe_int(p.get('heading1_size_pt', 16), 16)
    p['heading2_size_pt'] = _safe_int(p.get('heading2_size_pt', 14), 14)
    p['body_size_pt'] = _safe_float(p.get('body_size_pt', 12), 12)
    # 支持倍率行距或固定磅值（>3 视为固定 pt）
    p['body_line_spacing'] = _safe_float(p.get('body_line_spacing', 1.5), 1.5)
    p['body_indent_pt'] = _safe_float(p.get('body_indent_pt', 24), 24)
    return p


def _build_html_snapshot_for_check(bid_title: str, sections, preset: dict) -> str:
    """基于当前导出配置生成 HTML 快照，供格式清标校验。"""
    spec = docx_to_html_spec(preset)
    margins = spec.get('margins', {})
    margin_css = (
        f"{margins.get('top', 2.54)}cm {margins.get('right', 3.18)}cm "
        f"{margins.get('bottom', 2.54)}cm {margins.get('left', 3.18)}cm"
    )
    heading_css = spec.get('heading1', 'font-size:16pt;font-family:黑体;font-weight:bold;')
    body_css = spec.get(
        'body',
        f"font-size:{preset.get('body_size_pt', 12)}pt;font-family:{preset.get('body_font', '宋体')};"
        "line-height:26pt;text-indent:2em;color:#000;text-align:justify;"
    )
    table_header_css = spec.get('tableHeader', 'font-size:12pt;font-family:宋体;font-weight:bold;text-align:center;')
    table_body_css = spec.get('tableBody', 'font-size:12pt;font-family:宋体;text-align:left;')
    sample_title = sections[0][1] if sections else bid_title
    sample_content = ''
    if sections:
        sample_content = (sections[0][4] or sections[0][5] or '').strip()
    if not sample_content:
        sample_content = '本章内容用于导出格式校验。'
    lines = [x.strip() for x in sample_content.split('\n') if x.strip()]
    para_html = ''.join([f'<p style="{body_css}">{ln}</p>' for ln in lines[:3]]) or f'<p style="{body_css}">{sample_content}</p>'
    return (
        f'<!doctype html><html><head><style>'
        f'@page{{size:A4 portrait;margin:{margin_css};}}'
        f'table{{border-collapse:collapse;width:100%;}}td,th{{border:1px solid #d1d5db;padding:4pt;}}'
        f'</style></head><body style="margin:{margin_css};">'
        f'<h2 style="{heading_css}">{sample_title}</h2>'
        f'{para_html}'
        f'<table><thead><tr><th style="{table_header_css}">表头</th></tr></thead>'
        f'<tbody><tr><td style="{table_body_css}">表格内容</td></tr></tbody></table>'
        f'</body></html>'
    )


def _run_docx_html_roundtrip_check(bid_title: str, sections, preset: dict) -> dict:
    html = _build_html_snapshot_for_check(bid_title, sections, preset)
    return check_technical_bid(html)


def _repair_preset_from_check(preset: dict, check_result: dict) -> dict:
    """根据 HTML 格式检查结果回写修正 preset，再用于最终 Docx 导出。"""
    p = copy.deepcopy(preset or {})
    violations = check_result.get('violations', []) or []
    violated_rules = {v.get('rule') for v in violations}
    if '页面设置' in violated_rules:
        p['margins_cm'] = {'top': 2.54, 'bottom': 2.54, 'left': 3.18, 'right': 3.18}
    if '标题格式' in violated_rules:
        p['heading_font'] = '黑体'
        p['heading1_size_pt'] = 16
        p['heading2_size_pt'] = 14
    if '正文格式' in violated_rules:
        p['body_font'] = '宋体'
        p['body_size_pt'] = 14
        p['body_line_spacing'] = 26
        p['body_indent_pt'] = 28
    return _enforce_format_constraints(p)


# ── 图/表标记解析 ─────────────────────────────────────────────────────────

_BLOCK_PATTERN = re.compile(
    r'\[FIGURE:([^\]]+)\](.*?)\[/FIGURE\]'
    r'|\[TABLE:([^\]]+)\](.*?)\[/TABLE\]',
    re.DOTALL
)


def _strip_empty_lines_keep_indent(text: str) -> str:
    """仅移除首尾空行，保留行内与行首缩进，避免图示图素偏移。"""
    if text is None:
        return ''
    lines = text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
    while lines and lines[0].strip() == '':
        lines.pop(0)
    while lines and lines[-1].strip() == '':
        lines.pop()
    return '\n'.join(lines)


def _split_content_blocks(text: str) -> list:
    """
    将章节正文拆分为有序内容块列表：
      {'type': 'text',   'content': '...'}
      {'type': 'figure', 'title': '...', 'content': '...'}
      {'type': 'table',  'title': '...', 'content': '...'}
    """
    blocks = []
    last = 0
    for m in _BLOCK_PATTERN.finditer(text):
        if m.start() > last:
            blocks.append({'type': 'text', 'content': text[last:m.start()]})
        if m.group(1) is not None:
            blocks.append({'type': 'figure',
                           'title': m.group(1).strip(),
                           'content': _strip_empty_lines_keep_indent(m.group(2))})
        else:
            blocks.append({'type': 'table',
                           'title': m.group(3).strip(),
                           'content': m.group(4).strip()})
        last = m.end()
    if last < len(text):
        blocks.append({'type': 'text', 'content': text[last:]})
    return blocks


def _set_para_shading(para, hex_fill: str):
    """为段落设置背景填充色"""
    pPr = para._element.get_or_add_pPr()
    shd = OxmlElement('w:shd')
    shd.set(qn('w:val'), 'clear')
    shd.set(qn('w:color'), 'auto')
    shd.set(qn('w:fill'), hex_fill)
    pPr.append(shd)


def _set_cell_bg(cell, hex_fill: str):
    """为表格单元格设置背景色"""
    tc = cell._tc
    tcPr = 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_fill)
    tcPr.append(shd)


def _set_cell_padding(cell, pt_value: float):
    """设置表格单元格四侧内边距（单位：磅）"""
    tc = cell._tc
    tcPr = tc.get_or_add_tcPr()
    tcMar = OxmlElement('w:tcMar')
    val = str(int(pt_value * 20))   # pt → twips（1pt = 20 twips）
    for side in ('top', 'left', 'bottom', 'right'):
        node = OxmlElement(f'w:{side}')
        node.set(qn('w:w'), val)
        node.set(qn('w:type'), 'dxa')
        tcMar.append(node)
    tcPr.append(tcMar)


def _normalize_table_layout(table, align_center: bool = True):
    """统一表格布局，消除单侧偏移。"""
    try:
        table.alignment = WD_TABLE_ALIGNMENT.CENTER if align_center else WD_TABLE_ALIGNMENT.LEFT
    except Exception:
        pass
    try:
        table.autofit = True
    except Exception:
        pass
    for row in table.rows:
        for cell in row.cells:
            _set_cell_padding(cell, 0)
            for para in cell.paragraphs:
                para.paragraph_format.space_before = Pt(0)
                para.paragraph_format.space_after = Pt(0)
                para.paragraph_format.first_line_indent = Pt(0)
                para.paragraph_format.left_indent = Pt(0)
                para.paragraph_format.right_indent = Pt(0)


def _safe_set_eastasia(run, font_name: str):
    """安全设置东亚字体，确保 rPr 已存在"""
    _ = run.font.size  # 触发 rPr 创建
    try:
        run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
    except Exception:
        pass


def _add_block_caption(doc: Document, prefix: str, title: str):
    """添加图/表居中加粗标题行"""
    cap = doc.add_paragraph()
    cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
    cap.paragraph_format.space_before = Pt(8)
    cap.paragraph_format.space_after = Pt(3)
    run = cap.add_run(f'{prefix}：{title}')
    run.font.bold = True
    run.font.size = Pt(11)
    run.font.name = 'Times New Roman'
    _safe_set_eastasia(run, '黑体')


def _add_figure_block(doc: Document, title: str, content: str, preset=None, show_caption: bool = True):
    """
    将图示内容渲染为带边框 + 背景色的文字图示框。
    使用单格表格（Table Grid 样式）实现四周边框，比纯段落背景更专业。
    """
    if show_caption:
        _add_block_caption(doc, '图', title)

    if preset is None:
        preset = get_preset('standard')
    render_method = str(preset.get('figure_render_method', 'fixed_box')).strip().lower()
    if render_method not in ('fixed_box',):
        render_method = 'fixed_box'

    lines = content.replace('\r\n', '\n').replace('\r', '\n').split('\n')

    # 单格表格：四周边框 + 淡蓝灰背景
    tbl = doc.add_table(rows=1, cols=1)
    tbl.style = 'Table Grid'
    _normalize_table_layout(tbl, align_center=True)
    cell = tbl.cell(0, 0)
    _set_cell_bg(cell, 'EFF3FB')        # 淡蓝灰背景
    _set_cell_padding(cell, 0 if render_method == 'fixed_box' else 5)

    for i, line in enumerate(lines):
        if i == 0:
            para = cell.paragraphs[0]
            para.clear()
        else:
            para = cell.add_paragraph()
        para.paragraph_format.space_before = Pt(0)
        para.paragraph_format.space_after = Pt(0)
        para.paragraph_format.first_line_indent = Pt(0)
        para.paragraph_format.left_indent = Pt(0)
        para.paragraph_format.right_indent = Pt(0)
        para.paragraph_format.line_spacing = 1.0
        run = para.add_run(line if line else ' ')
        run.font.size = Pt(9.5)
        run.font.name = 'Consolas'
        _safe_set_eastasia(run, 'Consolas')

    # 图示后空行
    sp = doc.add_paragraph()
    sp.paragraph_format.space_after = Pt(8)


def _add_word_table(doc: Document, title: str, content: str, show_caption: bool = True, allow_text_fallback: bool = True):
    """将 Markdown 表格解析并渲染为 Word 表格"""
    # 解析 markdown 行，过滤掉分隔行（|---|）
    raw_rows = []
    for line in content.strip().split('\n'):
        line = line.strip()
        if not line:
            continue
        if re.match(r'^\|[\s\-:| ]+\|$', line):
            continue  # 分隔行
        if line.startswith('|') and line.endswith('|'):
            cells = [c.strip() for c in line[1:-1].split('|')]
            raw_rows.append(cells)

    if not raw_rows:
        # 没有解析到有效行时，降级为普通文本
        if show_caption:
            _add_block_caption(doc, '表', title)
        if allow_text_fallback:
            _add_plain_text(doc, content)
        return

    col_count = max(len(r) for r in raw_rows)
    rows = [r + [''] * (col_count - len(r)) for r in raw_rows]

    if show_caption:
        _add_block_caption(doc, '表', title)

    table = doc.add_table(rows=len(rows), cols=col_count)
    table.style = 'Table Grid'
    _normalize_table_layout(table, align_center=True)

    for i, row_data in enumerate(rows):
        for j, cell_text in enumerate(row_data):
            cell = table.cell(i, j)
            para = cell.paragraphs[0]
            para.clear()
            para.alignment = WD_ALIGN_PARAGRAPH.CENTER if i == 0 else WD_ALIGN_PARAGRAPH.LEFT
            run = para.add_run(cell_text)
            run.font.size = Pt(10)
            run.font.bold = (i == 0)
            run.font.name = 'Times New Roman'
            _safe_set_eastasia(run, '宋体')
            if i == 0:
                _set_cell_bg(cell, 'D6E4F7')  # 浅蓝表头

    # 表格后空行
    sp = doc.add_paragraph()
    sp.paragraph_format.space_after = Pt(6)


def _add_plain_text(doc: Document, text: str, preset=None):
    """添加普通文本段落（内部辅助），支持preset字体/大小"""
    if preset is None:
        preset = get_preset('standard')
    for line in text.split('\n'):
        line = line.strip()
        if not line:
            continue
        p = doc.add_paragraph()
        p.paragraph_format.first_line_indent = Pt(_safe_float(preset.get('body_indent_pt', 24), 24))
        p.paragraph_format.space_after = Pt(6)
        spacing = _safe_float(preset.get('body_line_spacing', 1.5), 1.5)
        if spacing > 3:
            p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
            p.paragraph_format.line_spacing = Pt(spacing)
        else:
            p.paragraph_format.line_spacing = spacing
        run = p.add_run(line)
        run.font.size = Pt(preset.get('body_size_pt', 12))
        run.font.name = preset.get('body_font', 'Times New Roman')
        _safe_set_eastasia(run, preset.get('body_font', '宋体'))


def _add_body_paragraphs(doc: Document, text: str, preset=None, attachment_only: bool = False, section_title: str = ''):
    """
    将正文文本分段渲染，自动识别并处理图示 [FIGURE:...] 和表格 [TABLE:...] 标记。
    支持从preset读取figure/table开关。
    """
    if preset is None:
        preset = get_preset('standard')
    blocks = _split_content_blocks(text)
    if attachment_only:
        # 附件章节导出硬限制：仅保留图/表块，其余文字全部忽略
        figure_table_blocks = [b for b in blocks if b['type'] in ('figure', 'table')]
        rules = att_sec.get_attachment_rules_cached()
        preferred_kind = att_sec.pick_single_figure_or_table(
            section_title or '',
            bool(preset.get('figure_enabled', True)),
            bool(preset.get('table_enabled', True)),
            rules,
        )
        if preferred_kind in ('figure', 'table'):
            preferred_blocks = [b for b in figure_table_blocks if b['type'] == preferred_kind]
            blocks = preferred_blocks[:1] if preferred_blocks else figure_table_blocks[:1]
        else:
            blocks = figure_table_blocks[:1]

    for block in blocks:
        if block['type'] == 'figure' and preset.get('figure_enabled', True):
            _add_figure_block(doc, block['title'], block['content'], preset, show_caption=not attachment_only)
        elif block['type'] == 'table' and preset.get('table_enabled', True):
            _add_word_table(
                doc,
                block['title'],
                block['content'],
                show_caption=not attachment_only,
                allow_text_fallback=not attachment_only,
            )
        elif not attachment_only:
            _add_plain_text(doc, block['content'], preset)


