from __future__ import annotations

import argparse
from pathlib import Path

from docx import Document
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor


INK = "18201E"
GREEN = "176B5B"
MUTED = "68756F"
PALE = "E7F2EE"
LINE = "DFE5E1"
WARNING = "FFF1D7"


def set_font(run, name="Arial Unicode MS", size=10.5, bold=False, color=INK):
    run.font.name = name
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.color.rgb = RGBColor.from_string(color)
    if run._element.get_or_add_rPr().rFonts is not None:
        fonts = run._element.get_or_add_rPr().rFonts
    else:
        fonts = OxmlElement("w:rFonts")
        run._element.get_or_add_rPr().append(fonts)
    for key in ("ascii", "hAnsi", "eastAsia"):
        fonts.set(qn(f"w:{key}"), name)


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


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


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


def add_numbering_definition(doc, num_id, abstract_id, kind):
    numbering = doc.part.numbering_part.element
    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)
    level = OxmlElement("w:lvl")
    level.set(qn("w:ilvl"), "0")
    start = OxmlElement("w:start")
    start.set(qn("w:val"), "1")
    level.append(start)
    number_format = OxmlElement("w:numFmt")
    number_format.set(qn("w:val"), "decimal" if kind == "number" else "bullet")
    level.append(number_format)
    level_text = OxmlElement("w:lvlText")
    level_text.set(qn("w:val"), "%1." if kind == "number" else "•")
    level.append(level_text)
    justification = OxmlElement("w:lvlJc")
    justification.set(qn("w:val"), "left")
    level.append(justification)
    p_pr = OxmlElement("w:pPr")
    tabs = OxmlElement("w:tabs")
    tab = OxmlElement("w:tab")
    tab.set(qn("w:val"), "num")
    tab.set(qn("w:pos"), "720")
    tabs.append(tab)
    p_pr.append(tabs)
    indent = OxmlElement("w:ind")
    indent.set(qn("w:left"), "720")
    indent.set(qn("w:hanging"), "360")
    p_pr.append(indent)
    level.append(p_pr)
    abstract.append(level)
    numbering.append(abstract)
    number = OxmlElement("w:num")
    number.set(qn("w:numId"), str(num_id))
    abstract_ref = OxmlElement("w:abstractNumId")
    abstract_ref.set(qn("w:val"), str(abstract_id))
    number.append(abstract_ref)
    numbering.append(number)


def new_numbering(doc, kind):
    current = getattr(doc, "_foodsafe_next_numbering", 980)
    add_numbering_definition(doc, current, current, kind)
    doc._foodsafe_next_numbering = current + 1
    return current


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


def set_alt_text(shape, description):
    doc_pr = shape._inline.docPr
    doc_pr.set("descr", description)
    doc_pr.set("title", description)


def add_title(doc):
    paragraph = doc.add_paragraph()
    paragraph.paragraph_format.space_before = Pt(14)
    paragraph.paragraph_format.space_after = Pt(8)
    run = paragraph.add_run("食安可视化部署中心")
    set_font(run, size=25, bold=True)
    subtitle = doc.add_paragraph()
    subtitle.paragraph_format.space_after = Pt(20)
    run = subtitle.add_run("实施工程师操作手册 · V1 演练版")
    set_font(run, size=12, color=GREEN, bold=True)

    table = doc.add_table(rows=3, cols=2)
    table.alignment = WD_TABLE_ALIGNMENT.LEFT
    table.autofit = False
    mark_header(table.rows[0])
    labels = (("适用角色", "实施工程师、交付负责人"), ("验证分支", "food_safety / test_jx206"), ("最后验证", "2026-09-03 · 精简版部署演练"))
    for index, (label, value) in enumerate(labels):
        table.columns[0].width = Inches(1.25)
        table.columns[1].width = Inches(4.85)
        cells = table.rows[index].cells
        shade_cell(cells[0], PALE)
        for cell in cells:
            set_cell_margins(cell)
            cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
        set_font(cells[0].paragraphs[0].add_run(label), size=9.5, bold=True, color=GREEN)
        set_font(cells[1].paragraphs[0].add_run(value), size=9.5)

    note = doc.add_paragraph()
    note.paragraph_format.space_before = Pt(18)
    note.paragraph_format.space_after = Pt(10)
    run = note.add_run("当前准入状态：演练模式已验证；实时部署、海光 3350/openEuler、Docker 离线运行时和 Mosquitto 仍需实机验收。")
    set_font(run, size=10.5, bold=True, color="724000")
    shading = OxmlElement("w:shd")
    shading.set(qn("w:fill"), WARNING)
    note._p.get_or_add_pPr().append(shading)


def add_heading(doc, text, level=1):
    paragraph = doc.add_heading(text, level=level)
    for run in paragraph.runs:
        set_font(run, size=16 if level == 1 else 12, bold=True, color=INK if level == 1 else GREEN)
    paragraph.paragraph_format.space_before = Pt(14 if level == 1 else 10)
    paragraph.paragraph_format.space_after = Pt(7)
    return paragraph


def add_body(doc, text, bold_lead=None):
    paragraph = doc.add_paragraph()
    paragraph.paragraph_format.space_after = Pt(6)
    paragraph.paragraph_format.line_spacing = 1.35
    if bold_lead and text.startswith(bold_lead):
        set_font(paragraph.add_run(bold_lead), bold=True)
        text = text[len(bold_lead):]
    set_font(paragraph.add_run(text))
    return paragraph


def add_code(doc, lines):
    for line in lines:
        paragraph = doc.add_paragraph()
        paragraph.paragraph_format.left_indent = Inches(0.18)
        paragraph.paragraph_format.space_after = Pt(1)
        shading = OxmlElement("w:shd")
        shading.set(qn("w:fill"), "F0F3F1")
        paragraph._p.get_or_add_pPr().append(shading)
        set_font(paragraph.add_run(line), name="Menlo", size=8.5, color=INK)


def add_numbered(doc, items):
    number_id = new_numbering(doc, "number")
    for item in items:
        paragraph = doc.add_paragraph()
        apply_numbering(paragraph, number_id)
        paragraph.paragraph_format.space_after = Pt(4)
        set_font(paragraph.add_run(item))


def add_bullets(doc, items):
    bullet_id = new_numbering(doc, "bullet")
    for item in items:
        paragraph = doc.add_paragraph()
        apply_numbering(paragraph, bullet_id)
        paragraph.paragraph_format.space_after = Pt(3)
        set_font(paragraph.add_run(item))


def add_image(doc, path, width, alt):
    paragraph = doc.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
    paragraph.paragraph_format.space_before = Pt(8)
    paragraph.paragraph_format.space_after = Pt(4)
    shape = paragraph.add_run().add_picture(str(path), width=Inches(width))
    set_alt_text(shape, alt)
    caption = doc.add_paragraph()
    caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
    caption.paragraph_format.space_after = Pt(10)
    set_font(caption.add_run(alt), size=8.5, color=MUTED)


def add_status_table(doc):
    rows = [
        ("无法读取远端分支", "Codeup SSH Key、网络或权限不可用", "在控制机验证 git ls-remote"),
        ("目标服务器未登记", "目标不在服务端白名单", "编辑本地 targets.json 后重启"),
        ("实时部署未启用", "执行开关仍关闭", "完成测试服务器登记和风险确认"),
        ("Docker缺失", "目标机无Docker，安装包无运行时", "提供经openEuler实机验证的运行时包"),
        ("架构不匹配", "目标不是x86_64", "更换amd64目标或构建arm64包"),
    ]
    table = doc.add_table(rows=1, cols=3)
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    widths = (Inches(1.45), Inches(2.2), Inches(2.45))
    headers = ("提示", "原因", "处理方式")
    mark_header(table.rows[0])
    for index, cell in enumerate(table.rows[0].cells):
        cell.width = widths[index]
        shade_cell(cell, INK)
        set_cell_margins(cell, top=120, bottom=120)
        cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
        run = cell.paragraphs[0].add_run(headers[index])
        set_font(run, size=9, bold=True, color="FFFFFF")
    for row_data in rows:
        row = table.add_row()
        for index, value in enumerate(row_data):
            cell = row.cells[index]
            cell.width = widths[index]
            set_cell_margins(cell, top=110, bottom=110)
            cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
            set_font(cell.paragraphs[0].add_run(value), size=8.8)


def build(template, output, desktop_top, desktop_complete, mobile_top):
    doc = Document(template)
    for section in doc.sections:
        for doc_properties in section.header._element.xpath(".//wp:docPr"):
            doc_properties.set("descr", "康比特品牌标识")
            doc_properties.set("title", "康比特品牌标识")
    styles = doc.styles
    normal = styles["Normal"]
    normal.font.name = "Arial Unicode MS"
    normal.font.size = Pt(10.5)
    normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Arial Unicode MS")

    add_title(doc)
    add_heading(doc, "1. 手册用途")
    add_body(doc, "食安可视化部署中心用于选择食品安全项目、Codeup分支和已登记目标服务器，并把部署过程显示为八个可追踪阶段。默认部署演练不会连接目标服务器。")
    add_heading(doc, "2. 启动前准备")
    add_bullets(doc, ["Node.js 20或更高版本。", "Git和可读取Codeup的SSH Key。", "实时部署还需要Docker Buildx、SSH、SCP和已登记测试服务器。"])
    add_code(doc, ["cd /Users/sunqianqian/Desktop/work/zhctprompt/foodSafetyDeploy", "npm test", "npm start", "浏览器：http://127.0.0.1:4173"])
    add_image(doc, desktop_top, 6.1, "图1 精简版部署中心桌面端入口")

    doc.add_page_break()
    add_heading(doc, "3. 创建部署演练")
    add_numbered(doc, [
        "选择“食品安全智慧管理云平台”。",
        "等待Codeup显示“已连接”。",
        "选择部署分支，当前默认test_jx206，并核对提交SHA。",
        "选择“海光3350演练环境”。",
        "核对选择框下方的一行部署摘要。",
        "点击“开始演练”；需要实时部署或Mosquitto时展开“高级选项”。",
    ])
    add_body(doc, "部署演练不会执行SSH、SCP、Docker或服务器写入。", bold_lead="部署演练")
    add_heading(doc, "4. 查看部署轨道")
    add_bullets(doc, ["源码准备 → 离线构建 → 制品校验 → 目标预检", "安全传输 → 服务安装 → 健康检查 → 完成", "状态同时显示文字和颜色，日志会自动遮盖常见密码和Token。"])
    add_image(doc, desktop_complete, 6.1, "图2 精简版部署轨道完成态和脱敏日志")

    doc.add_page_break()
    add_heading(doc, "5. 移动端查看")
    add_body(doc, "窄屏下，部署路径、配置、就绪清单和日志按任务顺序纵向排列，仍可完成整个演练流程。")
    add_image(doc, mobile_top, 2.45, "图3 精简版移动端部署入口")
    doc.add_page_break()
    add_heading(doc, "6. 开启实时部署")
    add_numbered(doc, [
        "复制config/targets.example.json为本地config/targets.json。",
        "登记目标内网IP、SSH用户、私钥绝对路径、远程目录和x86_64架构。",
        "复制.env.example为.env，设置DEPLOY_EXECUTION_ENABLED=true。",
        "使用openssl rand -hex 32生成管理令牌并写入DEPLOY_ADMIN_TOKEN。",
        "重启部署中心，在页面“访问设置”输入管理令牌。",
    ])

    doc.add_page_break()
    add_heading(doc, "7. 目标服务器没有Docker")
    add_body(doc, "构建控制机可通过FOODSAFE_HOST_RUNTIME_DIR指定经海光/openEuler实机验证的Docker离线运行时目录。生成.run时，该目录会一起封装。")
    add_code(doc, ["export FOODSAFE_HOST_RUNTIME_DIR=/受控路径/openeuler-24.03-x86_64-runtime"])
    add_body(doc, "没有Docker且安装包未携带已验证运行时时，安装器会停止，不联网下载，也不卸载Podman、containerd或runc。")
    add_heading(doc, "8. 常见提示")
    add_status_table(doc)

    add_heading(doc, "9. 当前限制")
    add_bullets(doc, [
        "海光3350/openEuler 24.03尚未完成本项目实机冷启动验证。",
        "openEuler Docker离线RPM不进入Git，必须放在受控制品库。",
        "Mosquitto未进入真实安装包；真实目标默认只允许core profile。",
        "本次没有连接或修改生产服务器。",
        "单块960GB SSD不提供数据冗余，生产建议RAID 1和异机备份。",
    ])

    add_heading(doc, "10. 验收结论")
    add_bullets(doc, [
        "Codeup真实分支读取通过，test_jx206已回读。",
        "部署演练主流程、桌面端和移动端检查通过。",
        "浏览器控制台0 error、0 warning。",
        "自解压安装包生成和SHA-256校验冒烟通过。",
        "真实服务器部署待测试服务器、Docker运行时和当前任务授权。",
    ])

    output.parent.mkdir(parents=True, exist_ok=True)
    doc.save(output)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("template", type=Path)
    parser.add_argument("output", type=Path)
    parser.add_argument("desktop_top", type=Path)
    parser.add_argument("desktop_complete", type=Path)
    parser.add_argument("mobile_top", type=Path)
    args = parser.parse_args()
    build(args.template, args.output, args.desktop_top, args.desktop_complete, args.mobile_top)


if __name__ == "__main__":
    main()
