from __future__ import annotations

import os
from pathlib import Path

from docx import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE, 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 Cm, Pt, RGBColor


ROOT = Path(
    os.environ.get(
        "CODE_AUDIT_OUTPUT_DIR",
        str(Path.home() / "Desktop/等保整改证明材料-20260916/01-代码审计"),
    )
)
STARTER = Path("/tmp/code-audit-compact-v2/starter.docx")
REPORT = ROOT / "01-代码审计情况报告及证明材料汇编-精简盖章版V2-盖章提交稿.docx"


def set_run_east_asia(run, font_name: str = "Hiragino Sans GB") -> None:
    r_pr = run._element.get_or_add_rPr()
    r_fonts = r_pr.find(qn("w:rFonts"))
    if r_fonts is None:
        r_fonts = OxmlElement("w:rFonts")
        r_pr.insert(0, r_fonts)
    r_fonts.set(qn("w:eastAsia"), font_name)


def set_cell_shading(cell, fill: str) -> None:
    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=55, start=75, bottom=55, end=75) -> None:
    tc_pr = cell._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 name, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
        node = tc_mar.find(qn(f"w:{name}"))
        if node is None:
            node = OxmlElement(f"w:{name}")
            tc_mar.append(node)
        node.set(qn("w:w"), str(value))
        node.set(qn("w:type"), "dxa")


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


def prevent_row_split(row) -> None:
    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 set_table_borders(table, color="D9D9D9", size="6") -> None:
    tbl_pr = table._tbl.tblPr
    borders = tbl_pr.first_child_found_in("w:tblBorders")
    if borders is None:
        borders = OxmlElement("w:tblBorders")
        tbl_pr.append(borders)
    for edge in ("top", "left", "bottom", "right", "insideH", "insideV"):
        elem = borders.find(qn(f"w:{edge}"))
        if elem is None:
            elem = OxmlElement(f"w:{edge}")
            borders.append(elem)
        elem.set(qn("w:val"), "single")
        elem.set(qn("w:sz"), size)
        elem.set(qn("w:color"), color)


def set_cell_text(cell, text: str, *, bold=False, color="000000", align=None, size=8.5) -> None:
    cell.text = ""
    p = cell.paragraphs[0]
    if align is not None:
        p.alignment = align
    p.paragraph_format.space_after = Pt(0)
    p.paragraph_format.line_spacing = 1.05
    run = p.add_run(text)
    run.bold = bold
    run.font.size = Pt(size)
    run.font.color.rgb = RGBColor.from_string(color)
    run.font.name = "Arial"
    set_run_east_asia(run)


def add_table(doc, headers, rows, widths, font_size=8.5):
    table = doc.add_table(rows=1, cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.autofit = False
    set_table_borders(table)
    set_repeat_header(table.rows[0])
    for i, header in enumerate(headers):
        cell = table.rows[0].cells[i]
        set_cell_shading(cell, "1F4E78")
        set_cell_text(cell, header, bold=True, color="FFFFFF", align=WD_ALIGN_PARAGRAPH.CENTER, size=8.0)
        cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
    for ri, row_data in enumerate(rows):
        cells = table.add_row().cells
        for i, value in enumerate(row_data):
            if ri % 2:
                set_cell_shading(cells[i], "F4F7FA")
            align = WD_ALIGN_PARAGRAPH.CENTER if len(str(value)) <= 12 else WD_ALIGN_PARAGRAPH.LEFT
            set_cell_text(cells[i], str(value), align=align, size=min(font_size, 8.0))
            cells[i].vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
    for row in table.rows:
        prevent_row_split(row)
        row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST
        for i, cell in enumerate(row.cells):
            set_cell_margins(cell)
            cell.width = Cm(widths[i])
    spacer = doc.add_paragraph()
    spacer.paragraph_format.space_after = Pt(0)
    return table


def configure_doc(doc: Document) -> None:
    for section in doc.sections:
        section.top_margin = Cm(2.54)
        section.bottom_margin = Cm(2.54)
        section.left_margin = Cm(3.175)
        section.right_margin = Cm(3.175)
        section.header_distance = Cm(1.2)
        section.footer_distance = Cm(1.0)
    normal = doc.styles["Normal"]
    normal.font.name = "Arial"
    normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Hiragino Sans GB")
    normal.font.size = Pt(9.8)
    normal.font.color.rgb = RGBColor(0, 0, 0)
    normal.paragraph_format.line_spacing = 1.25
    normal.paragraph_format.space_after = Pt(3)
    style_names = [style.name for style in doc.styles]
    if "Title" not in style_names:
        doc.styles.add_style("Title", WD_STYLE_TYPE.PARAGRAPH)
    title = doc.styles["Title"]
    title.font.name = "Arial"
    title._element.rPr.rFonts.set(qn("w:eastAsia"), "Hiragino Sans GB")
    title.font.size = Pt(20)
    title.font.bold = True
    title.font.color.rgb = RGBColor(0, 0, 0)
    title.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title.paragraph_format.space_before = Pt(16)
    title.paragraph_format.space_after = Pt(10)
    for name, size in (("Heading 1", 13), ("Heading 2", 11.2)):
        style = doc.styles[name]
        style.font.name = "Arial"
        style._element.rPr.rFonts.set(qn("w:eastAsia"), "Hiragino Sans GB")
        style.font.size = Pt(size)
        style.font.bold = True
        style.font.color.rgb = RGBColor(0, 0, 0)
        style.paragraph_format.keep_with_next = True
        style.paragraph_format.space_before = Pt(6)
        style.paragraph_format.space_after = Pt(3)


def body(doc, text: str, *, indent=True, bold_lead: str | None = None):
    p = doc.add_paragraph()
    if indent:
        p.paragraph_format.first_line_indent = Cm(0.74)
    if bold_lead and text.startswith(bold_lead):
        r = p.add_run(bold_lead)
        r.bold = True
        p.add_run(text[len(bold_lead):])
    else:
        p.add_run(text)
    return p


def evidence_line(doc, text: str, *, bold=False, size=7.8):
    p = doc.add_paragraph()
    p.paragraph_format.left_indent = Cm(0.35)
    p.paragraph_format.space_after = Pt(0.4)
    p.paragraph_format.line_spacing = 1.0
    run = p.add_run(text if text else " ")
    run.bold = bold
    run.font.name = "Arial"
    run.font.size = Pt(size)
    set_run_east_asia(run)
    return p


def page_heading(doc, text: str):
    p = doc.add_heading(text, level=1)
    p.paragraph_format.page_break_before = True
    return p


def build_report() -> None:
    if not STARTER.exists():
        raise FileNotFoundError(f"starter missing: {STARTER}")
    doc = Document(STARTER)
    configure_doc(doc)
    doc.core_properties.author = "北京康比特体育科技股份有限公司"
    doc.core_properties.last_modified_by = "北京康比特体育科技股份有限公司"
    doc.core_properties.title = "智慧营养健康餐厅管理系统代码审计情况报告及证明材料汇编"
    doc.core_properties.subject = "网络安全代码审计整改证明材料"
    doc.core_properties.keywords = "代码审计,等保,网络安全,证明材料"

    title = doc.paragraphs[0]
    title.style = doc.styles["Title"]
    title.alignment = WD_ALIGN_PARAGRAPH.CENTER
    title.add_run("智慧营养健康餐厅管理系统\n代码审计情况报告及证明材料汇编")
    meta = doc.add_paragraph()
    meta.alignment = WD_ALIGN_PARAGRAPH.CENTER
    meta.paragraph_format.space_after = Pt(10)
    run = meta.add_run("北京康比特体育科技股份有限公司  2026年9月10日")
    run.font.size = Pt(10.5)
    run.font.color.rgb = RGBColor(89, 89, 89)
    set_run_east_asia(run)

    body(doc, "本报告依据网络安全限期整改回函，对系统上线前未开展代码审计问题的阶段性落实情况进行说明。2026年9月10日已对指定源码快照开展重点代码与接口审查，并结合历史专项修复记录及服务器回读核验部署文件一致性。")

    doc.add_heading("一 问题与落实概况", level=1)
    add_table(doc, ["项目", "内容"], [
        ["回函问题", "系统上线前未开展代码审计。"],
        ["本次执行", "对指定源码快照开展身份鉴别、对象级授权、输入与SQL处理、敏感配置、会话令牌和密码存储重点审查。"],
        ["已取得结果", "形成审查范围、版本、方法和6类开放发现记录；复核1项历史人员详情授权修复的提交、定向测试及线上文件哈希一致性。"],
        ["当前结论", "首轮重点审查已实际开展并形成证据；开放发现仍需整改复核，不作全量代码审计完成或生产全系统通过结论。"],
    ], [3.0, 11.2], 9.0)

    doc.add_heading("二 审计对象、范围与方法", level=1)
    add_table(doc, ["项目", "实际内容"], [
        ["审查日期", "2026年9月10日"],
        ["源码版本", "分支 codex/csfzx-staff-info-horizontal-auth-20260824；提交 3428f1522d9b7c1a3dfed1c372963e0e15c25e44。"],
        ["覆盖范围", "登录与会话、人员接口、日期配餐、模型与校验、公共安全逻辑、系统及生产环境配置，以及历史专项测试记录。"],
        ["审查方法", "有限范围人工辅助静态检查；核对提交差异、定向测试记录和线上目标文件哈希。"],
        ["验证边界", "仅线上 Staff.php 与本地修复文件完成哈希一致性回读；未开展全量动态测试、第三方独立审计或生产全部接口验证。"],
    ], [3.0, 11.2], 8.9)

    doc.add_heading("三 主要发现与当前状态", level=1)
    finding_rows = [
        ["CA-01", "短信二次认证状态", "验证码状态由客户端Cookie承载且绑定关系不足。", "待整改复核"],
        ["CA-02", "人员更新删除等接口", "目标人员或部门范围的对象级授权仍有缺口。", "待整改复核"],
        ["CA-03", "日期配餐查询", "请求值进入动态SQL拼接路径。", "待参数化及动态复核"],
        ["CA-04", "敏感配置", "Git跟踪配置存在明文敏感凭据风险。", "待移出、轮换及历史评估"],
        ["CA-05", "会话令牌", "固定AES-CBC密钥与IV且部分路径缺少活动状态复核。", "待改造及环境核实"],
        ["CA-06", "密码存储", "双重MD5与低强度随机盐不足以抵抗离线破解。", "待兼容迁移及回归"],
    ]
    add_table(doc, ["编号", "审查点", "主要结果", "状态"], finding_rows, [1.5, 3.3, 6.2, 3.2], 8.1)
    body(doc, "上述6类发现均为本次首轮审查的内部跟踪项；截至本报告形成日，未取得其全部整改与复核证据，故不删除、不改写为已完成。", bold_lead="上述6类发现")

    doc.add_heading("四 已有修复与部署复核", level=1)
    add_table(doc, ["验证点", "实际证据", "结论"], [
        ["历史问题", "POST /p/staff/info 曾缺少目标人员数据范围校验。", "已形成专项修复"],
        ["代码修复", "提交3428f1522增加UUID规范化、许可人员集合校验和无权限阻断。", "提交差异可回读"],
        ["定向测试", "历史记录显示3项测试、10项断言通过。", "仅证明该接口定向回归"],
        ["部署一致性", "2026年9月10日服务器回读显示线上与本地 Staff.php SHA-256均为5cc6b23a99a3990f94c236b7d99378194c61a48057b101c460b55a5f5c22368c。", "修复文件已部署"],
        ["待补验证", "尚未使用专用受限账号完成线上合法对象与越权对象对照复测。", "不得称完整授权隔离验收通过"],
    ], [2.7, 8.0, 3.5], 8.3)

    section_five = doc.add_heading("五 安全开发与上线检查要求", level=1)
    section_five.paragraph_format.page_break_before = True
    body(doc, "公司现有《安全开发管理规定》已覆盖项目评审、安全需求审批、阶段评审、源代码与版本一致性、扫描与人工检查、问题整改以及上线前安全测试和安全评审；《组织及岗位职责管理规定》《变更管理规定》《授权和审批管理规定》提供职责、变更、测试和审批框架。相关制度用于证明后续控制要求，不代替本次执行记录。")

    doc.add_heading("六 关键证明材料目录", level=1)
    add_table(doc, ["编号", "证明材料", "支持结论"], [
        ["CA-EV-01", "审计版本、范围与发现记录", "证明2026年9月10日首轮重点代码审查实际开展。"],
        ["CA-EV-02", "人员详情接口修复与定向测试", "证明历史问题修复提交及定向回归结果。"],
        ["CA-EV-03", "部署文件一致性与制度依据", "证明修复文件已部署及后续安全开发控制依据。"],
    ], [2.0, 5.0, 7.2], 8.8)

    body(doc, "结论：截至2026年9月10日，已完成指定源码快照首轮重点代码与接口审查，形成6类开放发现，并完成历史人员详情授权修复的提交、定向测试和线上文件一致性核验。后续按发现清单继续整改和复核；本报告不作全量代码审计完成、第三方审计通过或生产全系统通过结论。", bold_lead="结论：")
    sign = doc.add_paragraph()
    sign.alignment = WD_ALIGN_PARAGRAPH.RIGHT
    sign.paragraph_format.space_before = Pt(16)
    sign.add_run("北京康比特体育科技股份有限公司（盖章）\n2026年9月10日")

    page_heading(doc, "附录一 审计版本、范围与发现记录")
    evidence_line(doc, "CA-EV-01 审计版本、范围与发现记录", bold=True, size=10.5)
    evidence_line(doc, "审查日期：2026-09-10")
    evidence_line(doc, "源码仓库：zhct/store业务仓库专项快照（内部留档）")
    evidence_line(doc, "仓库来源：公司代码仓库zhct/store")
    evidence_line(doc, "分支：codex/csfzx-staff-info-horizontal-auth-20260824")
    evidence_line(doc, "提交：3428f1522d9b7c1a3dfed1c372963e0e15c25e44")
    evidence_line(doc, "审查开始时工作树：clean")
    evidence_line(doc, "方法：有限范围人工辅助静态检查，核对历史专项测试及服务器文件哈希；未执行第三方全量审计。")
    evidence_line(doc, "覆盖：AdminController；Login、Staff、DateMenu控制器与逻辑；Staff模型；Login、Staff校验；common、config、database及csfzx生产配置；历史专项证据。")
    evidence_line(doc, "已确认控制：登录失败锁定、密码过期与复杂度检查、认证Cookie安全属性，以及 /p/staff/info 在详情查询前执行对象级UUID范围校验。")
    evidence_line(doc, "开放发现：CA-01至CA-06，分别涉及二次认证状态、对象级授权、动态SQL、敏感配置、会话令牌和密码存储；状态均以正文第三节为准。")
    evidence_line(doc, "边界：该路径为专项源码快照；除 Staff.php 文件一致性外，未建立整个提交与当前生产部署的完整版本映射。")

    page_heading(doc, "附录二 人员详情接口修复与定向测试")
    evidence_line(doc, "CA-EV-02 人员详情接口修复与定向测试", bold=True, size=10.5)
    evidence_line(doc, "历史问题：POST /p/staff/info 接收人员UUID时，缺少当前账号可访问人员范围校验。")
    evidence_line(doc, "修复提交：3428f1522d9b7c1a3dfed1c372963e0e15c25e44")
    evidence_line(doc, "提交说明：fix: 修复人员详情水平越权")
    evidence_line(doc, "修改文件：application/p/controller/Staff.php")
    evidence_line(doc, "关键代码摘录：", bold=True)
    for line in [
        "$uuid = trim((string)$uuid);",
        "$permittedStaffUuids = $this->resolvePermittedStaffUuids();",
        "if ($permittedStaffUuids !== null && !in_array($uuid, $permittedStaffUuids, true)) {",
        "    throw new Exception('没有数据权限');",
        "}",
    ]:
        evidence_line(doc, line, size=8.4)
    evidence_line(doc, "历史定向测试：OK (3 tests, 10 assertions)", bold=True)
    evidence_line(doc, "测试边界：该结果只覆盖人员详情接口定向测试；同批全量SecurityFixesTest曾有4项失败、1项错误，不能写成全量安全测试通过。")
    evidence_line(doc, "线上验证边界：修复文件已部署并完成哈希一致性回读；专用受限账号的合法/越权对象对照复测仍待补。")

    appendix_three = doc.add_heading("附录三 部署文件一致性与制度依据", level=1)
    appendix_three.paragraph_format.page_break_before = False
    evidence_line(doc, "CA-EV-03 部署文件一致性与制度依据", bold=True, size=10.5)
    evidence_line(doc, "服务器取证时间：2026-09-10 17:29 CST")
    evidence_line(doc, "线上文件：/workspace/wwwroot/zhct_csfzx/project/application/p/controller/Staff.php")
    evidence_line(doc, "本地文件：application/p/controller/Staff.php（提交3428f1522所在专项分支）")
    evidence_line(doc, "线上与本地文件SHA-256：", bold=True)
    evidence_line(doc, "5cc6b23a99a3990f94c236b7d99378194c61a48057b101c460b55a5f5c22368c", size=8.4)
    evidence_line(doc, "回读代码行为：info()在查询人员详情前校验UUID，并调用resolvePermittedStaffUuids()；UUID不在当前账号许可集合时返回“没有数据权限”。")
    evidence_line(doc, "证据结论：上述哈希一致性证明历史修复文件已部署，不等于专用受限账号线上对照复测通过。")
    evidence_line(doc, "制度依据：", bold=True)
    evidence_line(doc, "1. 《安全开发管理规定》第2至4页、第7至12页：安全需求与阶段评审、源代码与版本一致性、扫描与人工检查、问题整改及上线前安全测试和评审。")
    evidence_line(doc, "2. 《组织及岗位职责管理规定》第4页、第6页、第9至10页：安全检查、问题跟踪、审计复核和记录归档职责。")
    evidence_line(doc, "3. 《变更管理规定》第3至5页：变更影响分析、测试、回退、验收与记录要求。")
    evidence_line(doc, "4. 《授权和审批管理规定》第2至4页：程序变更、安全扫描与风险活动的授权审批要求。")
    evidence_line(doc, "制度使用边界：制度证明管理要求，不代替本次代码审查、修复提交、测试和服务器回读记录。")

    ROOT.mkdir(parents=True, exist_ok=True)
    doc.save(REPORT)
    print(REPORT)


if __name__ == "__main__":
    build_report()
