from pathlib import Path
import tempfile
import zipfile

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
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Inches, Pt, RGBColor


ROOT = Path(__file__).resolve().parent
WORKING_COPY = ROOT / "working-copy.docx"
OUTPUT = ROOT / "manual.docx"
SCREENSHOTS = ROOT / "screenshots"

FONT_NAME = "Noto Sans CJK SC"
ACCENT = "2F5F96"
LIGHT_BLUE = "D9EAF7"
PALE_BLUE = "F5F9FD"
LIGHT_GRAY = "D9D9D9"


def set_font(run, size=11, bold=False, color="000000"):
    run.font.name = FONT_NAME
    run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), FONT_NAME)
    run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), FONT_NAME)
    run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), FONT_NAME)
    run.font.size = Pt(size)
    run.font.bold = bold
    run.font.color.rgb = RGBColor.from_string(color)


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 margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
        node = tc_mar.find(qn(f"w:{margin}"))
        if node is None:
            node = OxmlElement(f"w:{margin}")
            tc_mar.append(node)
        node.set(qn("w:w"), str(value))
        node.set(qn("w:type"), "dxa")


def set_table_borders(table):
    borders = OxmlElement("w:tblBorders")
    for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
        node = OxmlElement(f"w:{edge}")
        node.set(qn("w:val"), "single")
        node.set(qn("w:sz"), "4")
        node.set(qn("w:color"), LIGHT_GRAY)
        borders.append(node)
    table._tbl.tblPr.append(borders)


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


def style_paragraph(paragraph, size=11, bold=False, color="000000", after=6, before=0, line=1.35):
    paragraph.paragraph_format.space_before = Pt(before)
    paragraph.paragraph_format.space_after = Pt(after)
    paragraph.paragraph_format.line_spacing = line
    for run in paragraph.runs:
        set_font(run, size, bold, color)
    return paragraph


def add_paragraph(doc, text="", size=11, bold=False, color="000000", after=6, before=0, line=1.35, align=None):
    paragraph = doc.add_paragraph()
    if align is not None:
        paragraph.alignment = align
    run = paragraph.add_run(text)
    set_font(run, size, bold, color)
    paragraph.paragraph_format.space_before = Pt(before)
    paragraph.paragraph_format.space_after = Pt(after)
    paragraph.paragraph_format.line_spacing = line
    return paragraph


def add_heading(doc, text, level=1):
    sizes = {1: 16, 2: 13}
    paragraph = doc.add_paragraph()
    paragraph.style = doc.styles["Heading 1" if level == 1 else "Heading 2"]
    paragraph.paragraph_format.keep_with_next = True
    paragraph.paragraph_format.space_before = Pt(10 if level == 1 else 7)
    paragraph.paragraph_format.space_after = Pt(5)
    run = paragraph.add_run(text)
    set_font(run, sizes[level], True, "000000")
    return paragraph


def add_bullet(doc, text, number=None):
    prefix = f"{number}. " if number is not None else "• "
    paragraph = add_paragraph(doc, prefix + text, size=10.5, after=4, line=1.3)
    paragraph.paragraph_format.left_indent = Inches(0.12)
    paragraph.paragraph_format.first_line_indent = Inches(-0.12)
    return paragraph


def add_table(doc, headers, rows, widths=None):
    table = doc.add_table(rows=1, cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    set_table_borders(table)
    header_props = table.rows[0]._tr.get_or_add_trPr()
    header_repeat = OxmlElement("w:tblHeader")
    header_repeat.set(qn("w:val"), "true")
    header_props.append(header_repeat)
    for index, value in enumerate(headers):
        cell = table.rows[0].cells[index]
        if widths:
            cell.width = Inches(widths[index])
        shade_cell(cell, ACCENT)
        set_cell_margins(cell)
        cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
        paragraph = cell.paragraphs[0]
        paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
        paragraph.paragraph_format.space_after = Pt(0)
        set_font(paragraph.add_run(value), 9.5, True, "FFFFFF")
    for row_index, values in enumerate(rows):
        cells = table.add_row().cells
        for index, value in enumerate(values):
            cell = cells[index]
            if widths:
                cell.width = Inches(widths[index])
            if row_index % 2:
                shade_cell(cell, PALE_BLUE)
            set_cell_margins(cell)
            cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
            paragraph = cell.paragraphs[0]
            paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER if index == 0 else WD_ALIGN_PARAGRAPH.LEFT
            paragraph.paragraph_format.space_after = Pt(0)
            paragraph.paragraph_format.line_spacing = 1.2
            set_font(paragraph.add_run(str(value)), 9.5)
    doc.add_paragraph().paragraph_format.space_after = Pt(2)
    return table


def add_figure(doc, image_name, caption):
    paragraph = doc.add_paragraph()
    paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
    paragraph.paragraph_format.keep_with_next = True
    run = paragraph.add_run()
    picture = run.add_picture(str(SCREENSHOTS / image_name), width=Inches(5.65))
    picture._inline.docPr.set("descr", caption)
    picture._inline.docPr.set("title", caption)
    caption_p = add_paragraph(doc, caption, size=9, color="666666", after=8, line=1.2, align=WD_ALIGN_PARAGRAPH.CENTER)
    caption_p.paragraph_format.keep_together = True


doc = Document(WORKING_COPY)

for paragraph in list(doc.paragraphs):
    if not paragraph.text.strip():
        doc._element.body.remove(paragraph._element)

styles = doc.styles
if "Title" not in styles:
    title_style = styles.add_style("Title", WD_STYLE_TYPE.PARAGRAPH)
    title_style.base_style = styles["Normal"]
for style_name in ("Normal", "Title", "Heading 1", "Heading 2"):
    if style_name in styles:
        style = styles[style_name]
        style.font.name = FONT_NAME
        style._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), FONT_NAME)
        style.font.color.rgb = RGBColor(0, 0, 0)

# Cover
cover = add_paragraph(doc, "AI营养智慧食堂管理平台", size=13, color=ACCENT, after=10, align=WD_ALIGN_PARAGRAPH.CENTER)
cover.paragraph_format.space_before = Pt(46)
title = add_paragraph(doc, "PC端操作手册", size=24, bold=True, after=8, align=WD_ALIGN_PARAGRAPH.CENTER)
title.style = doc.styles["Title"]
style_paragraph(title, size=24, bold=True, after=8, line=1.35)
subtitle = add_paragraph(doc, "膳食营养总览", size=20, bold=True, after=26, align=WD_ALIGN_PARAGRAPH.CENTER)
set_font(subtitle.runs[0], 20, True, ACCENT)

add_table(
    doc,
    ["项目", "内容"],
    [
        ("版本", "V1.1"),
        ("适用角色", "营养人员、后勤人员及具有菜单和数据权限的查看人员"),
        ("功能范围", "总览筛选、周期营养概况、自然周趋势、食谱安排浏览和当周食谱导出"),
        ("验证日期", "2026年9月28日"),
    ],
    widths=[1.25, 4.2],
)
doc.add_page_break()

add_heading(doc, "一 手册用途", 1)
add_paragraph(doc, "本手册说明如何在膳食营养总览中选择食堂、供餐对象和统计日期，查看能量、蛋白质、脂肪和碳水化合物的周期概况与自然周趋势，从食谱安排进入既有的编制或营养分析页面，并导出当前选中的当周食谱。")
add_paragraph(doc, "范围包含总览页的查看与筛选，以及从总览进入食谱管理后导出当前周食谱；不包含新建或编辑食谱、提交审核、审核发布、特殊供餐规则维护、营养科普与法规内容维护。总览页没有保存动作；日期筛选以“应用后检查”代替“保存后检查”。", color="555555")

add_heading(doc, "二 使用前提", 1)
for item in [
    "当前账号已登录 PC 管理后台，并具有膳食营养菜单权限。",
    "当前账号已获得目标学校和食堂的数据权限。",
    "如需导出食谱，当前账号还需具有“食谱导出”功能权限。",
    "基础设置中已存在可用的供餐对象。",
    "如需看到营养数据，所选日期内必须存在与食堂、供餐对象匹配的有效已发布食谱。",
]:
    add_bullet(doc, item)

add_heading(doc, "三 完整操作顺序", 1)
steps = [
    "进入“膳食营养”，打开顶部“总览”。",
    "在页面右上角确认或选择食堂和供餐对象。",
    "选择“近四周”“本月”或“自定义日期”；自定义日期选好起止日期后点击“应用”。",
    "查看四张营养卡片的数值、单位、参考范围和评价。",
    "点击营养卡片，在右侧查看相同筛选条件下的自然周趋势。",
    "向下查看食谱安排，按状态筛选并在需要时翻页。",
    "草稿或待审核食谱点击“查看编制”；已发布食谱点击“查看分析”。",
    "如需保存或打印当前周食谱，在食谱管理页面确认供餐周和当前食谱后点击“导出食谱”。",
]
for index, item in enumerate(steps, 1):
    add_bullet(doc, item, index)

doc.add_page_break()

add_heading(doc, "四 入口与范围选择", 1)
add_paragraph(doc, "菜单路径：膳食营养 → 总览。进入页面后，系统自动选择权限范围内的合法食堂和供餐对象，并默认统计近四周。只有一个供餐对象时，页面直接显示对象名称。", bold=False)

add_heading(doc, "选择食堂和供餐对象", 2)
for index, item in enumerate([
    "在页面右上角选择食堂。",
    "如该食堂有多个供餐对象，再选择供餐对象。",
    "等待营养概况和食谱安排完成刷新。",
], 1):
    add_bullet(doc, item, index)
add_paragraph(doc, "食堂或供餐对象变化后，营养卡片、周趋势、食谱状态计数和列表会同步切换；当前列表页码回到第1页。", size=10.5, color="555555")

add_heading(doc, "选择统计日期", 2)
for index, item in enumerate([
    "使用“近四周”查看默认周期。",
    "使用“本月”查看当月1日至当前日期。",
    "使用“自定义日期”时选择开始日期和结束日期，再点击“应用”。",
], 1):
    add_bullet(doc, item, index)
add_paragraph(doc, "统计日期只影响上方营养概况和周趋势，不会隐藏下方日期范围外的草稿、待审核或未来食谱。", size=10.5, color="555555")

add_heading(doc, "五 查看周期营养概况和周趋势", 1)
add_table(
    doc,
    ["卡片", "页面口径", "单位"],
    [
        ("能量", "计划供餐日的日均计划供给", "kcal"),
        ("蛋白质", "计划供餐日的日均计划供给", "g"),
        ("脂肪", "所选周期的供能比", "%"),
        ("碳水化合物", "所选周期的供能比", "%"),
    ],
    widths=[1.15, 3.55, 0.75],
)
add_paragraph(doc, "点击卡片后，右侧图表切换为对应指标。每个点按自然周与所选日期的交集统计；只有1周数据时提示“仅1周数据，暂无趋势”。缺失数据不会按0补齐，缺项周也不会与相邻周连接。", size=10.5)
add_figure(doc, "01-overview-published-real.png", "图1 真实测试页面中的已发布食谱、四项营养概况和单周数据点")

doc.add_page_break()

add_heading(doc, "六 查看食谱安排", 1)
for index, item in enumerate([
    "在“食谱安排”中选择“全部”“草稿”“待审核”或“已发布”。",
    "核对食谱名称、食堂与供餐对象、供餐周期和状态。",
    "数据超过一页时使用页码切换。",
    "草稿或待审核记录点击“查看编制”；已发布记录点击“查看分析”。",
], 1):
    add_bullet(doc, item, index)
add_paragraph(doc, "状态筛选只影响食谱列表，不改变上方营养统计。列表总数来自当前食堂和供餐对象下的完整结果，不受上方统计日期限制。", size=10.5, color="555555")

add_heading(doc, "导出当周食谱", 2)
for index, item in enumerate([
    "在食谱安排中找到目标记录。草稿或待审核食谱点击“查看编制”，已发布食谱点击“查看分析”。",
    "进入食谱管理页面后，确认供餐周、就餐学校档口和当前食谱均为目标范围。",
    "点击工具栏中的“导出食谱”。草稿、待审核和已发布的当前版本均可导出。",
    "使用 WPS 或 Excel 打开下载文件，核对单位、日期范围、周一至周日、餐次和菜品名称。",
], 1):
    add_bullet(doc, item, index)
add_paragraph(doc, "导出文件只包含当前选中的一个自然周，采用 A4 横向单页打印设置。文件不包含营养值、就餐人数、菜品 ID、配方倍数、采购量、成本或无来源的颜色标记。", size=10.5, color="555555")
add_figure(doc, "03-weekly-menu-export-preview.png", "图3 当周食谱导出结果左侧区域局部预览，实际文件继续横向显示至周日")

add_heading(doc, "七 无数据和异常提示", 1)
add_paragraph(doc, "如果所选日期内没有有效已发布食谱，四张卡片显示“—”或“数据不完整”，右侧显示“暂无已发布食谱数据”；下方草稿和待审核食谱仍可继续查看。")
add_figure(doc, "02-overview-no-published-real.png", "图2 真实测试页面中的无发布数据空态，状态计数仍正常保留")
add_paragraph(doc, "出现加载失败时，点击对应区域的“重新加载”。出现“暂无授权学校或食堂”时联系管理员配置数据权限；出现“当前食堂暂无合法供餐对象”时先到基础设置维护供餐对象。", size=10.5)

doc.add_page_break()

add_heading(doc, "八 应用后检查", 1)
add_paragraph(doc, "本页没有“保存”按钮。完成筛选或点击“应用”后，按以下顺序检查：")
for index, item in enumerate([
    "页面右上角的食堂和供餐对象是本次要查看的范围。",
    "营养概况标题下方显示的起止日期和供餐对象与所选条件一致。",
    "四张卡片和右侧趋势图已经刷新，图表标题与当前选中的营养卡片一致。",
    "自定义日期只改变营养区；下方食谱安排没有被日期裁剪。",
    "草稿或待审核进入编制页，已发布食谱进入营养分析页。",
    "执行导出后，文件日期范围与页面当前供餐周一致，并完整显示周一至周日和当前版本的餐次菜品。",
], 1):
    add_bullet(doc, item, index)

add_heading(doc, "九 必要字段说明", 1)
add_table(
    doc,
    ["字段或控件", "必须如何使用", "影响范围"],
    [
        ("食堂", "选择有权限的目标食堂", "营养概况、趋势、计数和列表"),
        ("供餐对象", "选择该食堂下的目标对象", "营养概况、趋势、计数和列表"),
        ("日期快捷项", "选择近四周、本月或自定义日期", "仅营养概况和趋势"),
        ("起止日期", "自定义日期时选择有效且不倒置的范围", "仅营养概况和趋势"),
        ("营养卡片", "点击要查看的营养指标", "仅切换右侧趋势指标"),
        ("食谱状态", "选择全部、草稿、待审核或已发布", "仅筛选列表并回到第1页"),
        ("导出食谱", "在食谱管理中导出当前选中的周食谱", "生成当前周横向 XLSX 文件，不改变食谱数据"),
    ],
    widths=[1.25, 2.55, 1.65],
)

doc.add_page_break()

add_heading(doc, "十 风险提示", 1)
for item in [
    "总览营养数据只统计有效已发布食谱。草稿和待审核食谱不会进入营养卡片和趋势计算。",
    "能量、蛋白质按计划供餐日计算日均；脂肪、碳水化合物按周期总量计算供能比。夜宵不进入正餐评价。",
    "缺项不补0。看到“数据不完整”时，应检查食谱、菜品和食材营养资料，不要把“—”解释为实际值0。",
    "只有1个自然周数据点时可以查看本周结果，但不能据此判断变化趋势。",
    "食堂和供餐对象受账号数据权限控制。页面没有展示某个范围，不代表该范围没有业务数据。",
    "“查看编制”和“查看分析”会进入食谱管理页面；后续能否编辑、提审或发布，以当前账号在目标页面的权限为准。",
    "“导出食谱”只读取当前或已发布版本，并继续校验学校和档口数据权限；按钮不可用时联系管理员检查“食谱导出”权限。",
    "导出只覆盖当前周，不支持跨周合并、批量导出、Word 或 PDF；空餐次食谱不能导出。",
    "显示“待专业资料完善”时，只读取实际数据，不自行作专业结论。",
]:
    add_bullet(doc, item)

doc.save(OUTPUT)

# python-docx may normalize an untouched header part during save. Restore the
# retained template's branded header package parts byte-for-byte.
preserve_parts = {
    "word/header1.xml",
    "word/_rels/header1.xml.rels",
    "word/media/image1.png",
}
with zipfile.ZipFile(WORKING_COPY, "r") as source_zip, zipfile.ZipFile(OUTPUT, "r") as final_zip:
    final_parts = {name: final_zip.read(name) for name in final_zip.namelist()}
    for name in preserve_parts:
        final_parts[name] = source_zip.read(name)
with tempfile.NamedTemporaryFile(dir=ROOT, suffix=".docx", delete=False) as temporary:
    temporary_path = Path(temporary.name)
try:
    with zipfile.ZipFile(temporary_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
        for name, payload in final_parts.items():
            archive.writestr(name, payload)
    temporary_path.replace(OUTPUT)
finally:
    if temporary_path.exists():
        temporary_path.unlink()

print(f"Generated {OUTPUT}")
