from __future__ import annotations

import os
from pathlib import Path

from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Pt

import build_official_v3 as fmt
import build_standard_v4 as v4


SOURCE_DIR = Path(
    os.environ.get(
        "CODE_AUDIT_DELIVERY_SOURCE_DIR",
        "/Users/wangshuo/CPT文件/资质认证/网络安全等级保护/回函材料/整改报告及证明",
    )
)
OUTPUT_DIR = SOURCE_DIR / "补充完善版"
FILE_NAME = "01-智慧营养健康餐厅管理系统代码安全审查记录及证明材料.docx"


def find_paragraph(doc: Document, text: str):
    matches = [paragraph for paragraph in doc.paragraphs if paragraph.text == text]
    if len(matches) != 1:
        raise ValueError(f"段落锚点数量不是 1：{text!r}，实际 {len(matches)}")
    return matches[0]


def replace_heading(doc: Document, old_text: str, new_text: str) -> None:
    paragraph = find_paragraph(doc, old_text)
    for run in paragraph.runs:
        run.text = ""
    run = paragraph.runs[0] if paragraph.runs else paragraph.add_run()
    run.text = new_text
    fmt.style_run(run, font=fmt.HEADING_FONT, size=16)


def style_code_cell(cell) -> None:
    paragraph = cell.paragraphs[0]
    paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
    paragraph.paragraph_format.line_spacing = Pt(12)
    for run in paragraph.runs:
        fmt.style_run(run, font="Menlo", size=8.2)


def add_detail_table(doc: Document, rows: list[list[str]]) -> None:
    fmt.add_table(doc, ["项目", "记录内容"], rows, [3.3, 12.2], font_size=9.8)
    table = doc.tables[-1]
    style_code_cell(table.rows[3].cells[1])
    doc.add_paragraph().paragraph_format.space_after = Pt(0)


def move_new_elements_before(doc: Document, anchor_text: str, start_index: int) -> None:
    anchor = find_paragraph(doc, anchor_text)
    body = doc._body._body
    new_elements = list(body)[start_index:]
    for element in new_elements:
        anchor._p.addprevious(element)


def build() -> None:
    source = SOURCE_DIR / FILE_NAME
    target = OUTPUT_DIR / FILE_NAME
    doc = Document(source)

    replace_heading(doc, "六、已提供的相关管理制度", "七、已提供的相关管理制度")
    replace_heading(doc, "七、结果说明及后续安排", "八、结果说明及后续安排")
    policy_intro = find_paragraph(
        doc,
        "下列管理制度文件已在前期材料中提供，本次列明其主要内容及与本项工作的对应关系。制度规定的流程和要求用于约束后续整改、测试、变更和复核，不据此推定相关执行事项已经全部完成。",
    )
    policy_intro.text = (
        "相关制度文件已在前期评审检查中提交，本次列明其主要内容及与本项工作的对应关系。制度规定的流程和要求用于约束后续整改、"
        "测试、变更和复核，不据此推定相关执行事项已经全部完成。"
    )
    fmt.style_run(policy_intro.runs[0], font=fmt.BODY_FONT, size=12.5)

    body = doc._body._body
    start_index = len(list(body)) - 1
    v4.heading(doc, "六、代表性代码检查明细")
    fmt.add_text(
        doc,
        "本节于2026年9月11日根据2026年9月10日重点代码只读检查记录及2026年8月24日人员详情接口历史修复、测试记录补充整理。"
        "所列代码仅保留与检查结论直接相关的短片段；未完成动态验证或线上复测的事项仍按原状态记录。",
        first_line=True,
        line_pt=22,
        size=13,
    )

    v4.heading(doc, "（一）人员详情接口对象范围校验", size=14, before=7, after=3)
    add_detail_table(
        doc,
        [
            ["检查事项", "人员详情查询前是否校验目标人员属于当前账号的数据权限范围。"],
            ["模块/函数及日期", "人员管理模块 Staff::info()；2026年9月10日核对，历史修复日期为2026年8月24日。"],
            [
                "实际代码逻辑",
                "$uuid = trim((string)$uuid);\n"
                "$permittedStaffUuids = $this->resolvePermittedStaffUuids();\n"
                "if ($permittedStaffUuids !== null\n"
                "    && !in_array($uuid, $permittedStaffUuids, true)) {\n"
                "    throw new Exception('没有数据权限');\n"
                "}",
            ],
            ["检查结论", "详情查询已在读取人员详细信息前增加目标人员范围校验；无权限目标在进入详情查询逻辑前被拒绝。该结论仅适用于人员详情子项，不扩大到更新、删除和展示等相关接口。"],
            ["关联修复/测试记录", "历史定向回归覆盖超级管理员兼容、授权范围内人员访问和范围外人员拒绝，共3项测试、10项断言通过；服务器目标文件已有部署检查记录。受限账号合法对象、越权对象的线上成对复测尚未完成。"],
        ],
    )

    v4.heading(doc, "（二）短信二次认证状态校验", size=14, before=7, after=3)
    add_detail_table(
        doc,
        [
            ["检查事项", "短信验证码状态是否由服务端与当前用户、会话可靠绑定。"],
            ["模块/函数及日期", "登录模块 Login::sendCode()、Login::checkCode()；2026年9月10日检查。"],
            [
                "实际代码逻辑",
                "$code = (string)random_int(100000, 999999);\n"
                "setcookie('mobile_code'.$uuid, $code, time()+120, '/', '');\n"
                "if (empty($_COOKIE['mobile_code'.$user_uuid]))\n"
                "    throw new Exception('请发送短信验证码');\n"
                "$user_code = $_COOKIE['mobile_code'.$user_uuid];\n"
                "if ($code !== $user_code)\n"
                "    throw new Exception('验证码输入错误，请重新输入');",
            ],
            ["检查结论", "验证码已使用随机六位数并设置短期有效时间，但校验值仍由客户端Cookie回传，服务端用户、会话绑定不足，原问题保持待整改复核。"],
            ["关联修复/测试记录", "相关安全测试集中包含验证码与登录Cookie检查，但同批结果仍有4项失败、1项错误；本项未形成新的动态复测通过记录。"],
        ],
    )

    v4.heading(doc, "（三）日期配餐查询条件构造", size=14, before=7, after=3)
    add_detail_table(
        doc,
        [
            ["检查事项", "日期、人员范围和附加条件是否采用参数化方式进入查询。"],
            ["模块/函数及日期", "日期配餐逻辑 DateMenu::getMenuList()、人员模型 Staff::getMenuList()；2026年9月10日检查。"],
            [
                "实际代码逻辑",
                """$staff_uuid_str = $staff_uuid_str.','."'".$staff_uuid."'";\n"""
                "$result = $this->staffModel->getMenuList(\n"
                "    $date, $staff_uuid_str, $page, $page_size, $where\n"
                ");\n"
                "if ($date)\n"
                "    $date_where = \"and  `b`.`menu_date` = '$date'\";\n"
                "if ($page_size != 10000000000)\n"
                "    $limit = \"limit $page,$page_size\";",
            ],
            ["检查结论", "人员标识列表、日期条件、附加条件和分页条件存在字符串拼接后进入模型查询的路径，未采用完整参数化处理。"],
            ["关联修复/测试记录", "现有记录为限定范围静态检查，未执行动态利用验证；尚无参数化整改和异常输入复测通过记录。"],
        ],
    )

    v4.heading(doc, "（四）口令摘要算法与盐值", size=14, before=7, after=3)
    add_detail_table(
        doc,
        [
            ["检查事项", "口令摘要算法和盐值生成强度是否满足当前安全要求。"],
            ["模块/函数及日期", "公共安全函数 get_salt()、get_password()；2026年9月10日检查。"],
            [
                "实际代码逻辑",
                "function get_salt() {\n"
                "    $str = '0123456789abcdefghijklmnopqrstuvwxyz';\n"
                "    for ($i=0; $i<6; $i++) {\n"
                "        $hash .= $str[mt_rand(0, $len-1)];\n"
                "    }\n"
                "    return $hash;\n"
                "}\n"
                "function get_password($password, $salt) {\n"
                "    return md5(md5($password).$salt);\n"
                "}",
            ],
            ["检查结论", "现有实现采用6位随机盐和双重MD5摘要，算法及盐值强度不足；需要制定兼容迁移方案，不能将密码复杂度、过期控制等其他措施视为该问题已解决。"],
            ["关联修复/测试记录", "截至本次材料修订，未形成口令摘要迁移及登录、改密、重置兼容回归通过记录，原问题保持待整改复核。"],
        ],
    )

    move_new_elements_before(doc, "七、已提供的相关管理制度", start_index)
    doc.core_properties.subject = "网络安全限期整改代码安全审查记录补充完善版"
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    doc.save(target)
    print(target)


if __name__ == "__main__":
    build()
