#!/usr/bin/env python3
from __future__ import annotations

import csv
import html
import importlib.util
import json
import shutil
import subprocess
import sys
import urllib.parse
import zipfile
from dataclasses import asdict
from datetime import datetime
from pathlib import Path

from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_COLOR_INDEX
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Pt, RGBColor
from PIL import Image, ImageDraw, ImageFont


REPO_ROOT = Path("/Users/jack/code/010-cpt/008-zhct/zhctprompt")
PROJECT_DIR = REPO_ROOT / "modules/presales-bidding/customer-projects/rehab-aids-weighing-system-20260614"
WORK_DIR = REPO_ROOT / "work/2026-06-14-rehab-aids-weighing-bid-chatgpt-pack"
DESKTOP = Path("/Users/jack/Desktop")
ASSET_DIR = PROJECT_DIR / "source-assets/graphic-rich-selected"
DERIVED_DIR = PROJECT_DIR / "source-assets/graphic-rich-derived"
DELIVERABLE_DIR = PROJECT_DIR / "deliverables"
REVIEW_DIR = PROJECT_DIR / "review"
DESKTOP_PACK = Path("/Users/jack/Desktop/rehab-aids-weighing-system-bid-chatgpt-pack-20260614")

DOC_BASENAME = "rehab-aids-weighing-system-bid-response-graphic-rich"
PROJECT_NAME = "智慧餐厅称重系统改造"
PURCHASER = "北京蓝色港湾科技有限责任公司"
SUPPLIER = "北京康比特运动营养科技有限公司（待最终投标主体确认）"


def load_formal_module():
    path = WORK_DIR / "generate_formal_long_bid_response.py"
    spec = importlib.util.spec_from_file_location("formal_long", path)
    if spec is None or spec.loader is None:
        raise RuntimeError(f"Cannot import {path}")
    mod = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = mod
    spec.loader.exec_module(mod)
    return mod


formal = load_formal_module()


def ensure_dirs() -> None:
    for d in [DERIVED_DIR, DERIVED_DIR / "insert-images", DERIVED_DIR / "certificate-previews", DELIVERABLE_DIR, REVIEW_DIR, WORK_DIR]:
        d.mkdir(parents=True, exist_ok=True)


def read_csv_dict(path: Path) -> list[dict[str, str]]:
    with path.open(encoding="utf-8-sig", newline="") as f:
        return list(csv.DictReader(f))


def set_cell_shading(cell, fill: str) -> None:
    tc_pr = cell._tc.get_or_add_tcPr()
    shd = OxmlElement("w:shd")
    shd.set(qn("w:fill"), fill)
    tc_pr.append(shd)


def set_cell_text(cell, text: str, bold: bool = False, highlight: bool = False) -> None:
    cell.text = ""
    p = cell.paragraphs[0]
    run = p.add_run(text)
    run.bold = bold
    run.font.name = "Microsoft YaHei"
    run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
    run.font.size = Pt(9)
    if highlight:
        run.font.highlight_color = WD_COLOR_INDEX.YELLOW


def is_uncertain(text: str) -> bool:
    markers = ["待", "需确认", "需补", "人工确认", "最终", "有效期", "授权", "商务确认", "服务资源", "另行确认", "以合同"]
    return any(m in text for m in markers)


def add_paragraph(doc: Document, text: str = "", *, style: str | None = None, bold: bool = False, highlight: bool | None = None) -> None:
    p = doc.add_paragraph(style=style)
    run = p.add_run(text)
    run.bold = bold
    run.font.name = "Microsoft YaHei"
    run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
    run.font.size = Pt(10.5)
    if highlight is None:
        highlight = is_uncertain(text)
    if highlight:
        run.font.highlight_color = WD_COLOR_INDEX.YELLOW


def add_heading(doc: Document, text: str, level: int = 1) -> None:
    p = doc.add_heading(level=level)
    run = p.add_run(text)
    run.font.name = "Microsoft YaHei"
    run._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
    run.font.color.rgb = RGBColor(20, 83, 95) if level == 1 else RGBColor(38, 50, 56)


def add_table(doc: Document, headers: list[str], rows: list[list[str]], widths: list[float] | None = None) -> None:
    table = doc.add_table(rows=1, cols=len(headers))
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    table.style = "Table Grid"
    for i, h in enumerate(headers):
        cell = table.rows[0].cells[i]
        set_cell_shading(cell, "D9EAF0")
        set_cell_text(cell, h, bold=True)
    for row in rows:
        cells = table.add_row().cells
        for i, value in enumerate(row):
            set_cell_text(cells[i], str(value), highlight=is_uncertain(str(value)))
    if widths:
        for row in table.rows:
            for idx, width in enumerate(widths):
                row.cells[idx].width = Cm(width)


def image_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    for fp in [
        "/System/Library/Fonts/PingFang.ttc",
        "/System/Library/Fonts/STHeiti Light.ttc",
        "/Library/Fonts/Arial Unicode.ttf",
    ]:
        try:
            return ImageFont.truetype(fp, size=size)
        except Exception:
            continue
    return ImageFont.load_default()


def create_project_architecture_diagram(out_path: Path) -> Path:
    w, h = 1800, 1120
    img = Image.new("RGB", (w, h), "white")
    d = ImageDraw.Draw(img)
    title_font = image_font(44)
    h_font = image_font(28)
    small = image_font(22)
    tiny = image_font(18)
    accent = (20, 83, 95)
    line = (112, 128, 144)
    pale = (229, 245, 247)
    yellow = (255, 244, 194)

    d.rectangle((0, 0, w, 105), fill=(239, 248, 249))
    d.text((70, 28), "智慧餐厅称重系统改造总体架构图", fill=accent, font=title_font)
    d.text((70, 74), "人员账户、称重终端、绑盘终端、消费设备、营养分析和院内系统接口统一闭环", fill=(55, 65, 81), font=small)

    layers = [
        ("用户与现场层", ["职工/会员手机端", "档口人员", "餐厅管理员", "售后运维人员"], 145),
        ("智能终端层", ["智能称重设备", "智能绑盘机", "双屏消费机", "称重消费机", "托盘/扫码盘"], 325),
        ("业务平台层", ["人员/部门/身份", "账户/补贴/钱包", "菜品/菜谱/营养", "订单/退款/报表", "设备/权限/日志"], 515),
        ("集成与数据层", ["HIS系统", "营养管理系统", "门禁系统", "停车系统", "数据统计/验收报表"], 705),
        ("保障体系", ["实施培训", "测试验收", "7*24服务", "质保维护", "证据附件"], 895),
    ]
    for title, boxes, y in layers:
        d.rounded_rectangle((60, y, 1740, y + 125), radius=18, outline=(203, 213, 225), width=3, fill=(248, 250, 252))
        d.text((90, y + 42), title, fill=accent, font=h_font)
        x = 355
        for box in boxes:
            d.rounded_rectangle((x, y + 25, x + 245, y + 96), radius=14, fill=pale, outline=(119, 173, 181), width=2)
            d.text((x + 20, y + 48), box, fill=(31, 41, 55), font=small)
            x += 265

    for y in [270, 455, 645, 835]:
        d.line((900, y, 900, y + 55), fill=line, width=5)
        d.polygon([(890, y + 55), (910, y + 55), (900, y + 78)], fill=line)

    d.rounded_rectangle((1110, 145, 1695, 270), radius=16, fill=yellow, outline=(245, 158, 11), width=2)
    d.text((1140, 172), "黄色风险边界", fill=(120, 53, 15), font=h_font)
    d.text((1140, 213), "接口免费开发、最终品牌型号、报价和证书有效期需人工确认", fill=(120, 53, 15), font=tiny)
    img.save(out_path, quality=94)
    return out_path


def render_pdf_first_page(pdf_path: Path, out_dir: Path) -> Path | None:
    out_prefix = out_dir / pdf_path.stem
    target = out_dir / f"{pdf_path.stem}-1.png"
    if target.exists():
        return target
    cmd = ["/opt/homebrew/bin/pdftoppm", "-f", "1", "-l", "1", "-png", "-r", "150", str(pdf_path), str(out_prefix)]
    try:
        subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError:
        pass
    if target.exists():
        return target
    matches = sorted(out_dir.glob(f"{pdf_path.stem}-*.png"))
    return matches[0] if matches else None


def prepare_image(asset: dict[str, str]) -> Path | None:
    src = Path(asset["local_copy"])
    if not src.exists():
        return None
    insert_dir = DERIVED_DIR / "insert-images"
    cert_dir = DERIVED_DIR / "certificate-previews"
    seq = int(asset["seq"])
    if src.suffix.lower() == ".pdf":
        rendered = render_pdf_first_page(src, cert_dir)
        if not rendered:
            return None
        src = rendered

    out = insert_dir / f"{seq:02d}-{asset['category']}-{asset['title']}.jpg"
    if out.exists():
        return out
    try:
        im = Image.open(src)
        im = im.convert("RGB")
        max_size = (1650, 1200)
        im.thumbnail(max_size)
        canvas = Image.new("RGB", (im.width, im.height), "white")
        canvas.paste(im, (0, 0))
        canvas.save(out, quality=88, optimize=True)
        return out
    except Exception:
        return None


def prepare_assets() -> list[dict[str, str]]:
    assets = read_csv_dict(ASSET_DIR / "graphic-rich-bid-selected-assets.csv")
    arch = {
        "seq": "0",
        "category": "architecture",
        "title": "本项目总体架构图",
        "source_path": "Codex基于采购文件和项目资料绘制",
        "local_copy": str(create_project_architecture_diagram(DERIVED_DIR / "project-specific-architecture.png")),
        "ext": ".png",
        "size_bytes": "0",
        "sha256": "",
    }
    assets.insert(0, arch)
    prepared: list[dict[str, str]] = []
    for asset in assets:
        img = prepare_image(asset)
        asset = dict(asset)
        asset["insert_image"] = str(img) if img else ""
        prepared.append(asset)
    with (DERIVED_DIR / "graphic-rich-bid-prepared-assets.csv").open("w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=list(prepared[0].keys()))
        writer.writeheader()
        writer.writerows(prepared)
    return prepared


def assets_by_category(assets: list[dict[str, str]], category: str) -> list[dict[str, str]]:
    return [a for a in assets if a["category"] == category and a.get("insert_image")]


def add_image(doc: Document, asset: dict[str, str], width_cm: float = 15.5) -> None:
    if not asset.get("insert_image"):
        add_paragraph(doc, f"图片缺失：{asset['title']}", highlight=True)
        return
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = p.add_run()
    run.add_picture(asset["insert_image"], width=Cm(fit_width_cm(asset["insert_image"], width_cm, 22.0)))
    cap = doc.add_paragraph()
    cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = cap.add_run(f"图：{asset['title']}")
    r.font.size = Pt(9)
    r.font.color.rgb = RGBColor(71, 85, 105)
    r.font.name = "Microsoft YaHei"
    r._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")


def add_image_grid(doc: Document, assets: list[dict[str, str]], cols: int = 2, width_cm: float = 7.6) -> None:
    if not assets:
        add_paragraph(doc, "本组图片待补。", highlight=True)
        return
    for start in range(0, len(assets), cols):
        chunk = assets[start:start + cols]
        table = doc.add_table(rows=2, cols=cols)
        table.alignment = WD_TABLE_ALIGNMENT.CENTER
        for c, asset in enumerate(chunk):
            cell = table.rows[0].cells[c]
            cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
            p = cell.paragraphs[0]
            p.alignment = WD_ALIGN_PARAGRAPH.CENTER
            if asset.get("insert_image"):
                p.add_run().add_picture(asset["insert_image"], width=Cm(fit_width_cm(asset["insert_image"], width_cm, 8.0)))
            cap = table.rows[1].cells[c]
            set_cell_text(cap, f"图：{asset['title']}", bold=False)
        for c in range(len(chunk), cols):
            table.rows[0].cells[c].text = ""
            table.rows[1].cells[c].text = ""
        doc.add_paragraph()


def fit_width_cm(image_path: str, max_width_cm: float, max_height_cm: float) -> float:
    try:
        with Image.open(image_path) as im:
            w, h = im.size
        if w <= 0 or h <= 0:
            return max_width_cm
        aspect = w / h
        return min(max_width_cm, max_height_cm * aspect)
    except Exception:
        return max_width_cm


def configure_doc(doc: Document) -> None:
    section = doc.sections[0]
    section.page_width = Cm(21)
    section.page_height = Cm(29.7)
    section.top_margin = Cm(1.8)
    section.bottom_margin = Cm(1.8)
    section.left_margin = Cm(1.8)
    section.right_margin = Cm(1.8)
    styles = doc.styles
    styles["Normal"].font.name = "Microsoft YaHei"
    styles["Normal"]._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
    styles["Normal"].font.size = Pt(10.5)
    for style_name, size, color in [
        ("Heading 1", 17, RGBColor(20, 83, 95)),
        ("Heading 2", 14, RGBColor(31, 41, 55)),
        ("Heading 3", 12, RGBColor(55, 65, 81)),
    ]:
        st = styles[style_name]
        st.font.name = "Microsoft YaHei"
        st._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
        st.font.size = Pt(size)
        st.font.color.rgb = color
        st.font.bold = True


def add_footer(doc: Document) -> None:
    section = doc.sections[0]
    footer = section.footer.paragraphs[0]
    footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
    run = footer.add_run("国家康复辅具研究中心智慧餐厅称重系统改造投标响应文件（图文增强编制稿）")
    run.font.size = Pt(8)
    run.font.color.rgb = RGBColor(100, 116, 139)


def create_docx(items, assets: list[dict[str, str]]) -> Path:
    doc = Document()
    configure_doc(doc)
    add_footer(doc)

    add_heading(doc, "国家康复辅具研究中心智慧食堂项目", 1)
    p = doc.add_paragraph()
    p.alignment = WD_ALIGN_PARAGRAPH.CENTER
    r = p.add_run("智慧餐厅称重系统改造投标响应文件")
    r.font.size = Pt(24)
    r.bold = True
    r.font.name = "Microsoft YaHei"
    r._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
    add_paragraph(doc, "文件版本：图文增强正式编制稿 V2.0")
    add_paragraph(doc, f"采购人：{PURCHASER}")
    add_paragraph(doc, f"供应商：{SUPPLIER}", highlight=True)
    add_paragraph(doc, "投标报价：人民币【待商务确认】元（含税，不超过150000元）", highlight=True)
    add_paragraph(doc, "编制日期：2026年6月14日")
    add_image(doc, assets_by_category(assets, "architecture")[0], width_cm=15.5)
    doc.add_page_break()

    add_heading(doc, "投标文件总目录", 1)
    toc_rows = [
        ["第一册", "商务响应文件", "投标函、报价、资格、商务响应、偏离说明"],
        ["第二册", "技术响应文件", "项目理解、总体架构、软件功能、接口边界"],
        ["第三册", "硬件设备响应", "称重设备、绑盘机、消费设备、托盘、利旧事项"],
        ["第四册", "项目实施、测试、培训与质量保障", "实施计划、现场勘查、联调、验收、培训"],
        ["第五册", "售后服务与运维保障", "质保、7*24响应、4小时到场、备品备件"],
        ["第六册", "资质证书、软著、专利与合规附件", "软著证书、专利、CNAS、国产化认证"],
        ["第七册", "类似案例与图文证据", "历史项目现场照片和应用场景"],
        ["附录A", "逐项技术响应明细", "PC端、手机端、硬件、售后、接口逐项响应"],
        ["附录B", "人工确认清单", "黄色标注、补证和签章清稿清单"],
    ]
    add_table(doc, ["册别", "标题", "内容范围"], toc_rows, [2.0, 5.0, 9.5])
    add_paragraph(doc, "说明：文档中黄色标注均表示正式递交前必须由商务、投标主体、产品、硬件、售后或法务人工确认的内容。", highlight=True)
    doc.add_page_break()

    add_heading(doc, "第一册 商务响应文件", 1)
    add_heading(doc, "一、投标函", 2)
    add_paragraph(doc, f"致：{PURCHASER}")
    add_paragraph(doc, f"我方已认真阅读采购文件，现就“{PROJECT_NAME}”提交投标响应文件。")
    add_paragraph(doc, "如我方成交，将按照采购文件、响应文件和合同约定完成供货、安装、调试、测试、培训、试运行、验收和质保服务。")
    add_heading(doc, "二、报价及商务响应", 2)
    add_table(doc, ["条款", "采购文件要求", "响应", "需确认点"], [
        ["预算控制", "15万元含税以内", "响应", "最终报价、税率、大小写金额待商务确认"],
        ["投标有效期", "截止日期后90日历日", "响应", "按采购文件格式盖章"],
        ["付款方式", "到货30%、验收20%、使用3个月45%、质保期结束5%", "响应", "是否存在合同偏离待商务确认"],
        ["报价范围", "设备、运输、包装、调试、软件、接口、维保等", "响应", "分项报价和品牌型号待确认"],
    ], [3.2, 5.5, 2.0, 5.5])
    add_heading(doc, "三、资格与附件清单", 2)
    add_table(doc, ["材料", "当前处理", "正式递交前动作"], [
        ["营业执照/信用截图", "当前未自动嵌入", "投标主体提供并盖章"],
        ["法定代表人身份证明/授权委托书", "保留格式位置", "按采购文件第五章模板填写"],
        ["软著/专利/CNAS/国产化证书", "已在第六册嵌入预览", "核对有效期、适用范围和外发授权"],
        ["类似业绩证明", "已列出候选项目和现场图", "补合同/验收/中标证明并确认脱敏"],
    ], [4.5, 4.0, 7.5])
    doc.add_page_break()

    add_heading(doc, "第二册 技术响应文件", 1)
    add_heading(doc, "一、项目理解与建设目标", 2)
    add_paragraph(doc, "本项目不是单点硬件采购，而是围绕职工就餐、称重计量、托盘绑定、消费结算、营养分析、运营管理和院内系统对接的一体化智慧餐厅改造。")
    add_paragraph(doc, "核心目标包括减少浪费、提升职工体验、降低食堂运营复杂度、实现营养健康数据可视化，并为后续HIS、营养管理、门禁、停车等系统对接预留标准接口。")
    add_heading(doc, "二、总体架构图", 2)
    for a in assets_by_category(assets, "architecture")[:2]:
        add_image(doc, a, width_cm=16.5)
    add_heading(doc, "三、业务流程图", 2)
    for a in assets_by_category(assets, "architecture")[2:4]:
        add_image(doc, a, width_cm=14.8)
    add_heading(doc, "四、软件功能图文响应", 2)
    add_image_grid(doc, assets_by_category(assets, "software"), cols=2, width_cm=7.2)
    add_table(doc, ["功能域", "采购要求映射", "图文证据"], [
        ["人员与卡务", "机构人员、卡片生命周期、身份管理", "人员管理/卡务管理截图"],
        ["账户与补贴", "现金充值、补贴发放、二/三级审核", "账户管理和补贴统计截图"],
        ["订单与报表", "消费订单、退款、充值、菜品销量、收入汇总", "订单管理和统计报表截图"],
        ["营养健康", "个体/团体膳食监控、营养日报、阶段报告", "PC营养统计和移动端营养日报截图"],
        ["权限与安全", "账号、角色、人脸库、日志留存", "系统管理和人脸管理截图"],
    ], [3.8, 6.2, 6.0])
    add_paragraph(doc, "手机端、小程序或公众号入口的最终名称、访问方式、软著对应关系需要投标主体和产品负责人确认。", highlight=True)
    doc.add_page_break()

    add_heading(doc, "第三册 硬件设备响应", 1)
    add_heading(doc, "一、硬件设备总览", 2)
    add_image_grid(doc, assets_by_category(assets, "hardware")[:8], cols=2, width_cm=7.4)
    add_heading(doc, "二、智能称重设备", 2)
    add_image_grid(doc, [a for a in assets_by_category(assets, "hardware") if "450" in a["title"] or "460" in a["title"] or "称重结算" in a["title"]], cols=2, width_cm=7.4)
    add_table(doc, ["采购要求要点", "响应说明", "证据"], [
        ["称重范围、精度、识盘速度、离线同步", "按智能称重设备参数响应；现场逐台联调验收", "设备原图、尺寸图、功能截图、参数页"],
        ["菜品营养展示和摄入建议", "与后台菜品营养库和人员档案联动", "终端界面、PC营养统计、移动端营养日报"],
        ["异常报警与防逃费", "支持余额不足、托盘未绑定、压秤等异常提示", "终端操作截图和验收测试记录"],
        ["最终品牌型号", "当前按可供产品线组织响应", "品牌型号和厂家授权待正式确认"],
    ], [4.5, 6.0, 5.0])
    add_heading(doc, "三、智能绑盘机", 2)
    add_image_grid(doc, [a for a in assets_by_category(assets, "hardware") if "绑盘" in a["title"]], cols=2, width_cm=7.4)
    add_paragraph(doc, "智能绑盘机通过人脸、卡、条码或二维码完成身份识别和托盘绑定，支持人员名单、人脸照片、卡片数据同步，并支持多种绑定方式配置。")
    add_heading(doc, "四、双屏消费机、称重消费机与小卖部消费设备", 2)
    add_image_grid(doc, [a for a in assets_by_category(assets, "hardware") if "消费机" in a["title"] or "收银机" in a["title"]], cols=2, width_cm=7.4)
    add_paragraph(doc, "双屏消费机和称重消费机应覆盖刷卡、人脸、二维码消费、断网收银、订单查询、语音播报、商品分类、报表查询等能力。")
    add_heading(doc, "五、托盘与利旧事项", 2)
    tray = [a for a in assets_by_category(assets, "hardware") if "托盘" in a["title"]]
    add_image_grid(doc, tray, cols=2, width_cm=7.4)
    add_paragraph(doc, "采购文件要求白色PP平托盘带码，并与现有托盘保持一致；现有称重称7台、扫码盘100个需与新系统对接或按采购要求处理。", highlight=True)
    add_paragraph(doc, "利旧兼容性需现场核验设备型号、通讯协议、扫码盘编码规则和现有数据口径，不能仅凭历史资料直接承诺全部无风险兼容。", highlight=True)
    doc.add_page_break()

    add_heading(doc, "第四册 项目实施、测试、培训与质量保障", 1)
    add_table(doc, ["阶段", "工作内容", "交付物"], [
        ["项目启动", "确认范围、角色、计划、风险和沟通机制", "启动会纪要、项目计划"],
        ["现场勘查", "核验档口、电源、网络、既有设备、动线和接口条件", "勘查记录、点位表"],
        ["设备到货", "设备验收、编号、安装、联网、基础配置", "到货验收单、安装照片"],
        ["系统部署", "后台、手机端、终端、权限、基础数据配置", "配置清单、账号清单"],
        ["接口联调", "HIS、营养、门禁、停车接口字段和连通性测试", "接口测试记录、问题清单"],
        ["试运行验收", "功能测试、异常测试、数据一致性和用户培训", "测试报告、培训签到、验收单"],
    ], [3.0, 8.0, 5.0])
    add_paragraph(doc, "接口免费二次开发应限定为采购文件范围内、第三方提供接口文档和测试环境前提下的标准对接；非标准改造、历史数据治理和第三方系统改造需另行确认。", highlight=True)
    add_heading(doc, "培训方案", 2)
    add_table(doc, ["培训对象", "培训内容", "验收方式"], [
        ["管理员", "人员、账户、补贴、权限、报表、设备管理", "现场操作考核"],
        ["档口人员", "绑盘、称重、消费、异常处理", "模拟交易演练"],
        ["财务/后勤", "订单、退款、充值、补贴和报表导出", "业务场景测试"],
        ["运维人员", "设备巡检、网络排障、日志和报修", "问题闭环演练"],
    ], [3.5, 8.0, 4.5])
    doc.add_page_break()

    add_heading(doc, "第五册 售后服务与运维保障", 1)
    add_paragraph(doc, "产品质保期不低于1年，自验收合格之日起计算。质保期内因产品质量问题造成的故障，由供应商负责维修或更换。")
    add_paragraph(doc, "提供7*24小时技术咨询服务；电话支持不能排除故障时，按采购文件要求安排工程师携带备件到场处理。", highlight=True)
    add_table(doc, ["服务事项", "响应机制", "需补充确认"], [
        ["咨询与报修", "7*24服务热线、微信群或工单渠道", "正式联系人和电话"],
        ["现场维修", "远程不能解决时安排现场支持", "4小时到场资源和备件库存"],
        ["软件升级", "质保期内缺陷修复和版本优化", "终身免费升级边界"],
        ["备品备件", "电源、扫码模块、读卡模块、网络配件", "备件型号和供货周期"],
    ], [4.0, 6.5, 5.5])
    doc.add_page_break()

    add_heading(doc, "第六册 资质证书、软著、专利与合规附件", 1)
    add_paragraph(doc, "以下证据来自企业微信微盘项目资料或数字技术中心知识库。本编制稿仅嵌入首页预览或图片证据；正式递交前必须核对证书完整页、有效期、权利人、适用产品和外发授权。", highlight=True)
    certs = assets_by_category(assets, "certificate")
    for asset in certs:
        add_heading(doc, asset["title"], 2)
        add_image(doc, asset, width_cm=14.5)
        add_paragraph(doc, f"来源：{asset['source_path']}", highlight=is_uncertain(asset["source_path"]))
    doc.add_page_break()

    add_heading(doc, "第七册 类似案例与图文证据", 1)
    add_paragraph(doc, "采购文件评分项要求近三年智慧食堂类似项目业绩。以下图片仅用于说明公司历史项目能力；正式作为业绩证明时，应补合同、中标通知书、验收报告或客户证明，并确认脱敏和外发授权。", highlight=True)
    add_image_grid(doc, assets_by_category(assets, "case"), cols=2, width_cm=7.6)
    add_table(doc, ["案例方向", "可证明能力", "正式投标需补"], [
        ["智慧餐厅现场建设", "多档口餐台、称重取餐、现场运营", "合同/验收/客户授权"],
        ["运动营养智慧餐厅", "营养健康和膳食管理场景", "系统截图、项目说明、脱敏授权"],
        ["历史标书材料", "技术方案、售后方案、培训方案复用", "按本项目采购文件重写，不直接照搬"],
    ], [4.0, 6.0, 6.0])
    doc.add_page_break()

    add_heading(doc, "附录A 逐项技术响应明细", 1)
    for item in items:
        p = doc.add_paragraph()
        p.paragraph_format.page_break_before = True
        r = p.add_run(f"{item.code} {item.category}：{item.title}")
        r.bold = True
        r.font.size = Pt(13)
        r.font.name = "Microsoft YaHei"
        r._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
        rows = [
            ["采购要求", item.requirement],
            ["响应承诺", item.response],
            ["实现方式", item.implementation],
            ["验收方式", item.acceptance],
            ["证据/附件", item.evidence],
        ]
        add_table(doc, ["项目", "内容"], rows, [3.0, 13.0])

    doc.add_section(WD_SECTION.NEW_PAGE)
    add_heading(doc, "附录B 黄色标注与人工确认清单", 1)
    add_table(doc, ["类别", "需要人工确认的原因", "责任人建议"], [
        ["报价与付款", "AI无法替代商务报价、税率、合同偏离判断", "商务/投标负责人"],
        ["投标主体", "公司名称、授权代表、盖章主体必须与递交文件一致", "投标负责人/法务"],
        ["硬件型号", "设备品牌、供货型号、授权文件和检测报告须与实际供货一致", "产品/硬件负责人"],
        ["接口范围", "免费二次开发需限定在接口文档、测试环境、验收标准明确的范围内", "产品/研发/商务"],
        ["证书有效性", "软著、专利、CNAS、国产化证书需核验原件、有效期和外发授权", "投标负责人/法务"],
        ["案例外发", "历史项目照片和案例资料须脱敏并确认客户授权", "销售/交付负责人"],
    ], [4.0, 8.5, 4.0])

    out = DELIVERABLE_DIR / f"{DOC_BASENAME}.docx"
    doc.save(out)
    return out


def create_markdown(items, assets: list[dict[str, str]]) -> Path:
    path = DELIVERABLE_DIR / f"{DOC_BASENAME}.md"
    lines = [
        "# 图文增强版投标响应文件编制说明",
        "",
        f"- 项目：{PROJECT_NAME}",
        f"- 采购人：{PURCHASER}",
        f"- 供应商：{SUPPLIER}",
        "- 版本：图文增强正式编制稿 V2.0",
        f"- 逐项响应条款数：{len(items)}",
        f"- 引用资产数：{len(assets)}",
        "",
        "## 图文增强策略",
        "",
        "- 架构部分：嵌入项目总体架构图、业务流程拓扑图和绑盘/打餐流程图。",
        "- 硬件部分：嵌入企业微盘标准产品和供应商资料中的原始设备图片。",
        "- 软件部分：嵌入 PC 端和手机端真实系统截图。",
        "- 软著/专利/证书：渲染 PDF 首页作为附件预览，并保留原始文件来源路径。",
        "- 黄色标注：所有待商务确认、待投标主体确认、待补证据、接口边界和外发授权事项在 Word 内做黄色高亮。",
        "",
        "## 引用资产",
        "",
        "| 序号 | 类别 | 标题 | 来源 |",
        "| --- | --- | --- | --- |",
    ]
    for a in assets:
        lines.append(f"| {a['seq']} | {a['category']} | {a['title']} | `{a['source_path']}` |")
    path.write_text("\n".join(lines) + "\n", encoding="utf-8")
    return path


def create_review_html(assets: list[dict[str, str]], page_count: int, image_count: int, yellow_count: int) -> Path:
    cards = []
    for a in assets:
        if not a.get("insert_image"):
            continue
        img = Path(a["insert_image"])
        if img.exists():
            src = "file://" + urllib.parse.quote(str(img))
            cards.append(f"""
            <figure>
              <img src="{src}" alt="{html.escape(a['title'])}">
              <figcaption>{html.escape(a['category'])} / {html.escape(a['title'])}</figcaption>
            </figure>
            """)
    html_text = f"""<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <title>国家康复辅具称重系统投标图文增强版人审页</title>
  <style>
    body {{ margin:0; font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif; color:#1f2937; background:#f8fafc; }}
    header {{ background:#0f4c5c; color:white; padding:36px 48px; }}
    main {{ max-width:1180px; margin:0 auto; padding:32px; }}
    section {{ background:white; border:1px solid #e5e7eb; border-radius:8px; padding:24px; margin:18px 0; }}
    h1,h2 {{ margin:0 0 14px; }}
    .stats {{ display:grid; grid-template-columns:repeat(4,1fr); gap:12px; }}
    .stat {{ background:#eef6f7; border:1px solid #cfe5e8; border-radius:8px; padding:16px; }}
    .stat strong {{ display:block; font-size:26px; color:#0f4c5c; }}
    .warn {{ background:#fff7cc; border-left:5px solid #f59e0b; padding:12px 14px; }}
    .gallery {{ display:grid; grid-template-columns:repeat(3,1fr); gap:16px; }}
    figure {{ margin:0; border:1px solid #e5e7eb; border-radius:8px; padding:10px; background:#fff; }}
    img {{ width:100%; height:210px; object-fit:contain; background:#f8fafc; }}
    figcaption {{ font-size:13px; color:#475569; margin-top:8px; line-height:1.45; }}
    ul {{ line-height:1.8; }}
  </style>
</head>
<body>
  <header>
    <h1>国家康复辅具研究中心智慧餐厅称重系统改造投标响应文件</h1>
    <p>图文增强正式编制稿人审页</p>
  </header>
  <main>
    <section class="stats">
      <div class="stat"><strong>{page_count}</strong>PDF页数</div>
      <div class="stat"><strong>{image_count}</strong>嵌入图片</div>
      <div class="stat"><strong>{yellow_count}</strong>黄色标注</div>
      <div class="stat"><strong>{len(assets)}</strong>引用资产</div>
    </section>
    <section>
      <h2>必须人工确认</h2>
      <div class="warn">报价、税率、投标主体、盖章签字、证书有效期、厂家授权、硬件品牌型号、接口免费开发边界、4小时到场服务资源、历史案例外发授权仍需人工确认。</div>
    </section>
    <section>
      <h2>审阅重点</h2>
      <ul>
        <li>目录是否符合正式投标文件的册别结构。</li>
        <li>硬件图片是否与最终供货型号一致。</li>
        <li>软著、专利、CNAS、国产化证书是否能用于本项目外发。</li>
        <li>黄色标注内容是否已经替换成最终商务、产品和法务确认版本。</li>
      </ul>
    </section>
    <section>
      <h2>引用图片预览</h2>
      <div class="gallery">
        {''.join(cards)}
      </div>
    </section>
  </main>
</body>
</html>
"""
    out = REVIEW_DIR / "rehab-aids-weighing-system-bid-graphic-rich-review.html"
    out.write_text(html_text, encoding="utf-8")
    return out


def convert_pdf(docx: Path) -> tuple[Path, int]:
    pdf_dir = WORK_DIR / "pdf-check"
    pdf_dir.mkdir(exist_ok=True)
    subprocess.run([
        "/opt/homebrew/bin/soffice",
        "--headless",
        "--convert-to",
        "pdf",
        "--outdir",
        str(pdf_dir),
        str(docx),
    ], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    pdf = pdf_dir / f"{docx.stem}.pdf"
    page_count = 0
    try:
        out = subprocess.check_output(["pdfinfo", str(pdf)], text=True)
        for line in out.splitlines():
            if line.startswith("Pages:"):
                page_count = int(line.split(":", 1)[1].strip())
                break
    except Exception:
        pass
    return pdf, page_count


def count_docx_media_and_highlights(docx: Path) -> tuple[int, int]:
    import zipfile
    with zipfile.ZipFile(docx) as z:
        media_count = len([n for n in z.namelist() if n.startswith("word/media/")])
        xml = z.read("word/document.xml").decode("utf-8", errors="ignore")
        yellow_count = xml.count('w:highlight w:val="yellow"')
    return media_count, yellow_count


def create_zip(files: list[Path]) -> Path:
    out = DESKTOP / f"{DOC_BASENAME}-20260614.zip"
    if out.exists():
        out.unlink()
    with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
        for f in files:
            if f.exists():
                z.write(f, f.name)
        for f in [
            ASSET_DIR / "graphic-rich-bid-selected-assets.csv",
            DERIVED_DIR / "graphic-rich-bid-prepared-assets.csv",
            ASSET_DIR / "graphic-rich-selected-assets-contact-sheet.jpg",
        ]:
            if f.exists():
                z.write(f, f.name)
        for raw in ASSET_DIR.iterdir():
            if raw.is_file() and raw.suffix.lower() in [".pdf", ".png", ".jpg", ".jpeg"]:
                z.write(raw, f"selected-source-assets/{raw.name}")
    return out


def main() -> None:
    ensure_dirs()
    formal.copy_indexes()
    items = formal.build_requirement_items()
    assets = prepare_assets()
    docx = create_docx(items, assets)
    md = create_markdown(items, assets)
    pdf, page_count = convert_pdf(docx)
    media_count, yellow_count = count_docx_media_and_highlights(docx)
    html_review = create_review_html(assets, page_count, media_count, yellow_count)

    desktop_docx = DESKTOP / docx.name
    desktop_pdf = DESKTOP / pdf.name
    desktop_html = DESKTOP / html_review.name
    shutil.copy2(docx, desktop_docx)
    shutil.copy2(pdf, desktop_pdf)
    shutil.copy2(html_review, desktop_html)
    zip_path = create_zip([docx, md, pdf, html_review])

    summary = {
        "generated_at": datetime.now().isoformat(timespec="seconds"),
        "docx": str(docx),
        "pdf": str(pdf),
        "desktop_docx": str(desktop_docx),
        "desktop_pdf": str(desktop_pdf),
        "review_html": str(html_review),
        "desktop_zip": str(zip_path),
        "requirement_items": len(items),
        "prepared_assets": len(assets),
        "embedded_media_count": media_count,
        "yellow_highlight_count": yellow_count,
        "pdf_pages": page_count,
    }
    (WORK_DIR / "graphic-rich-bid-response-summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
    (WORK_DIR / "graphic-rich-bid-response-summary.md").write_text(
        "# 图文增强版投标响应文件生成记录\n\n"
        f"- Word：`{docx}`\n"
        f"- PDF：`{pdf}`\n"
        f"- HTML 人审页：`{html_review}`\n"
        f"- 桌面 ZIP：`{zip_path}`\n"
        f"- PDF 页数：{page_count}\n"
        f"- 嵌入图片数：{media_count}\n"
        f"- 黄色标注数：{yellow_count}\n"
        f"- 逐项响应条款数：{len(items)}\n"
        f"- 引用资产数：{len(assets)}\n\n"
        "## 说明\n\n"
        "本版针对用户反馈重做为图文增强正式编制稿，插入架构图、流程图、硬件原图、系统截图、软著/专利/CNAS/国产化证书预览和案例现场图。"
        "黄色高亮表示正式递交前必须人工确认或替换的内容。\n",
        encoding="utf-8",
    )
    print(json.dumps(summary, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
