#!/usr/bin/env python3
"""Build the self-contained local HTML gallery from the reviewed CSV bases."""

from __future__ import annotations

import csv
import json
from collections import defaultdict
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data"
OUTPUT = ROOT / "public" / "index.html"


def read_csv(name: str) -> list[dict[str, str]]:
    with (DATA / name).open(encoding="utf-8-sig", newline="") as handle:
        return list(csv.DictReader(handle))


def unique(values: list[str]) -> list[str]:
    seen: set[str] = set()
    result: list[str] = []
    for value in values:
        value = value.strip()
        if value and value not in seen:
            seen.add(value)
            result.append(value)
    return result


def build_projects() -> list[dict[str, object]]:
    selections = read_csv("project-image-selection.csv")
    bases = {row["项目ID"]: row for row in read_csv("project-case-base.csv")}

    people: dict[str, list[dict[str, str]]] = defaultdict(list)
    for row in read_csv("people-detail.csv"):
        people[row["项目ID"]].append(row)

    software: dict[str, list[dict[str, str]]] = defaultdict(list)
    for row in read_csv("software-detail.csv"):
        software[row["项目ID"]].append(row)

    hardware: dict[str, list[dict[str, str]]] = defaultdict(list)
    for row in read_csv("hardware-detail.csv"):
        hardware[row["项目ID"]].append(row)

    projects: list[dict[str, object]] = []
    for selection in selections:
        project_id = selection["项目ID"]
        base = bases[project_id]
        image = selection["唯一主图"]
        image_path = ROOT / image
        if not image_path.is_file():
            raise FileNotFoundError(f"Missing selected image: {image_path}")

        actual_software = unique([
            row["软件模块"]
            for row in software[project_id]
            if row["事实/建议"] != "场景建议"
        ])
        suggested_software = unique([
            row["软件模块"]
            for row in software[project_id]
            if row["事实/建议"] == "场景建议"
        ])

        actual_hardware: list[dict[str, str]] = []
        for row in hardware[project_id]:
            if row["事实/建议"] == "场景建议":
                continue
            quantity = row["数量原文"].strip()
            unit = row["单位"].strip()
            actual_hardware.append({
                "name": row["硬件名称"].strip(),
                "model": (row["标准型号"] or row["项目写法/型号"]).strip(),
                "quantity": f"{quantity}{unit}" if quantity else "数量待补证",
                "grade": row["证据等级"].strip(),
                "mapping": row["映射状态"].strip(),
            })

        metrics: list[dict[str, str]] = []
        metric_fields = [
            ("注册账户", "已观察注册账户", "个"),
            ("日日活", "已观察日日活", "人"),
            ("30日活跃", "已观察30日活跃", "人"),
            ("累计订单", "累计有效订单", "笔"),
            ("近30日订单", "近30日订单", "笔"),
        ]
        for label, field, unit in metric_fields:
            value = base[field].strip()
            if value:
                metrics.append({"label": label, "value": value, "unit": unit})

        correction = selection["纠偏说明"].strip()
        if project_id == "P028":
            correction = (
                "照片错配已纠正：原图误用了P005城市副中心照片，已隔离；"
                "本卡仅使用证据占位图，禁止引用隔离图。"
            )
            if "quarantine" in image or "misattributed" in image:
                raise ValueError("P028 must never select a quarantined or misattributed image")

        projects.append({
            "id": project_id,
            "name": selection["项目名称"],
            "shortName": base["简称"],
            "scene": selection["场景类型"],
            "sceneSummary": base["一句话场景"],
            "grade": selection["总体证据等级"],
            "oneToOne": selection["是否可1比1"],
            "image": "../" + image,
            "imageStatus": selection["主图状态"],
            "people": [item.strip() for item in base["服务人群"].split("、") if item.strip()],
            "metrics": metrics,
            "problems": base["主要问题"],
            "actualSoftware": actual_software,
            "suggestedSoftware": suggested_software,
            "actualSoftwareFallback": base["实际软件观察"],
            "hardwareCountBasis": selection["数量证据"] or base["设备观察口径"],
            "hardware": actual_hardware,
            "frontFlow": base["前厅主流程"],
            "kitchenFlow": base["后厨主流程"],
            "blocker": selection["正式出图阻断"],
            "boundary": base["验证能力/证据边界"],
            "correction": correction,
            "sourceTable": selection["源证据表"],
        })

    if len(projects) != 28:
        raise ValueError(f"Expected 28 projects, found {len(projects)}")
    images = [project["image"] for project in projects]
    if len(images) != len(set(images)):
        raise ValueError("Every project must have a unique selected image")
    return projects


HTML_HEAD = r'''<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>智慧食堂 · 28项目场景证据图册</title>
  <style>
    :root {
      --paper:#f5f7f4; --surface:#ffffff; --surface-2:#eef2ee; --ink:#12201c;
      --muted:#61706a; --line:#d9e0dc; --green:#0d684e; --green-2:#e2f1ea;
      --blue:#225f85; --gold:#9b6b19; --red:#a03c2f; --shadow:0 12px 36px rgba(22,48,38,.08);
      --radius:22px; --radius-sm:12px;
    }
    *{box-sizing:border-box} html{scroll-behavior:smooth}
    body{margin:0;background:var(--paper);color:var(--ink);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;line-height:1.55}
    button,input,select{font:inherit} button{cursor:pointer}
    .page{width:min(1480px,calc(100% - 32px));margin:auto}
    .hero{padding:58px 0 28px;border-bottom:1px solid var(--line)}
    .eyebrow{font-size:13px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--green)}
    h1{font-size:clamp(34px,5vw,66px);line-height:1.04;letter-spacing:-.045em;margin:14px 0 18px;max-width:950px}
    .hero-copy{font-size:18px;color:var(--muted);max-width:900px;margin:0}
    .principle{margin-top:28px;padding:18px 22px;border:1px solid #b9d6ca;border-radius:16px;background:linear-gradient(135deg,#fff,#eaf4ef);display:flex;align-items:center;gap:16px}
    .principle strong{font-size:18px;color:var(--green);white-space:nowrap}.principle span{color:var(--muted)}
    .summary{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin:24px 0 32px}
    .summary-card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:16px 18px;box-shadow:0 6px 20px rgba(22,48,38,.04)}
    .summary-card b{display:block;font-size:28px;line-height:1}.summary-card span{display:block;color:var(--muted);font-size:12px;margin-top:8px}
    .toolbar-wrap{position:sticky;top:0;z-index:30;padding:12px 0;background:rgba(245,247,244,.92);backdrop-filter:blur(16px);border-bottom:1px solid rgba(217,224,220,.85)}
    .toolbar{display:grid;grid-template-columns:minmax(260px,1fr) 220px auto auto;gap:10px}
    .field{height:46px;border:1px solid var(--line);border-radius:12px;background:var(--surface);padding:0 14px;color:var(--ink);min-width:0}
    .grade-filter{display:flex;gap:6px}.grade-filter button{height:46px;min-width:46px;border:1px solid var(--line);background:var(--surface);border-radius:12px;color:var(--muted);font-weight:800}
    .grade-filter button.active{background:var(--ink);border-color:var(--ink);color:#fff}
    .result-count{height:46px;display:flex;align-items:center;padding:0 8px;color:var(--muted);font-size:13px;white-space:nowrap}
    .cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:22px;padding:28px 0 56px}
    .case{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow);display:flex;flex-direction:column;min-width:0}
    .case.hidden{display:none}.media{position:relative;aspect-ratio:16/9;background:#e9eeeb;overflow:hidden}.media img{width:100%;height:100%;object-fit:cover;display:block}.media::after{content:"";position:absolute;inset:auto 0 0;height:38%;background:linear-gradient(transparent,rgba(7,21,16,.42));pointer-events:none}
    .badges{position:absolute;z-index:2;top:14px;left:14px;right:14px;display:flex;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:7px 10px;border-radius:999px;background:rgba(255,255,255,.94);box-shadow:0 4px 16px rgba(0,0,0,.10);font-size:12px;font-weight:800}.grade-A{color:#086044}.grade-B{color:#1d5f83}.grade-C{color:#865f18}.grade-D{color:#953a30}.one-no{color:var(--red)}.one-part{color:var(--gold)}.one-yes{color:var(--green)}
    .image-status{position:absolute;z-index:2;bottom:12px;left:14px;right:14px;color:#fff;font-size:12px;text-shadow:0 1px 4px rgba(0,0,0,.5)}
    .case-body{padding:22px;display:flex;flex-direction:column;gap:18px;flex:1}
    .case-title-row{display:flex;align-items:flex-start;justify-content:space-between;gap:18px}.project-id{color:var(--green);font-size:12px;font-weight:900;letter-spacing:.12em}.case h2{font-size:25px;line-height:1.18;letter-spacing:-.025em;margin:4px 0 5px}.scene{font-size:13px;color:var(--muted)}
    .scene-summary{font-size:15px;font-weight:750;text-align:right;max-width:245px;color:var(--green)}
    .section{border-top:1px solid var(--line);padding-top:15px}.section-title{font-size:12px;font-weight:900;letter-spacing:.08em;color:var(--muted);margin-bottom:9px;text-transform:uppercase}
    .tags{display:flex;flex-wrap:wrap;gap:7px}.tag{padding:5px 9px;background:var(--surface-2);border-radius:8px;font-size:12px;color:#42514b}.tag.fact{background:#e2f1ea;color:#155d46}.tag.suggest{background:#edf2f5;color:#285974}
    .metrics{display:flex;flex-wrap:wrap;gap:9px;margin-top:10px}.metric{padding:8px 10px;border:1px solid var(--line);border-radius:10px;min-width:94px}.metric b{font-size:16px}.metric span{display:block;color:var(--muted);font-size:10px}
    .software-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.software-box{padding:12px;border-radius:12px;border:1px solid var(--line);background:#fafbfa}.software-box h3{font-size:12px;margin:0 0 8px}.software-box p{font-size:12px;color:var(--muted);margin:0}.software-box.actual{border-left:4px solid var(--green)}.software-box.suggested{border-left:4px solid var(--blue)}
    .hardware-basis{padding:12px 14px;border-radius:12px;background:#f7f3ea;border:1px solid #e5d8be;color:#5f4a25;font-size:13px}.hardware-basis strong{color:#805d18}
    .hardware-list{margin:10px 0 0;padding:0;list-style:none;display:grid;grid-template-columns:1fr 1fr;gap:7px}.hardware-list li{font-size:12px;padding:8px 9px;border:1px solid var(--line);border-radius:9px;color:#42514b}.hardware-list b{display:block;color:var(--ink)}
    .flow-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}.flow{position:relative;padding:12px 12px 12px 40px;border-radius:12px;background:var(--surface-2);font-size:12px;color:#42514b}.flow::before{position:absolute;left:12px;top:12px;width:20px;height:20px;border-radius:7px;display:grid;place-items:center;color:#fff;font-size:11px;font-weight:900}.flow.front::before{content:"前";background:var(--green)}.flow.back::before{content:"后";background:var(--blue)}
    .blocker{padding:13px 14px;border:1px solid #efc8c2;background:#fff4f2;border-radius:12px;color:#79352c;font-size:13px}.blocker strong{display:block;color:#a03c2f;margin-bottom:3px}
    .correction{padding:13px 14px;border:1px solid #c2d8e4;background:#eff7fb;border-radius:12px;color:#285974;font-size:13px}.correction strong{display:block;color:#225f85;margin-bottom:3px}
    details{border-top:1px dashed var(--line);padding-top:12px}summary{font-size:12px;font-weight:800;color:var(--green);cursor:pointer}.detail-copy{margin:10px 0 0;color:var(--muted);font-size:12px}.detail-meta{margin-top:8px;font-size:11px;color:#87928d}
    .empty{grid-column:1/-1;text-align:center;padding:70px 20px;color:var(--muted)}
    footer{border-top:1px solid var(--line);padding:28px 0 42px;color:var(--muted);font-size:12px;display:flex;justify-content:space-between;gap:18px}
    @media(max-width:1050px){.summary{grid-template-columns:repeat(3,1fr)}.toolbar{grid-template-columns:1fr 180px}.grade-filter,.result-count{grid-row:2}.cards{grid-template-columns:1fr}}
    @media(max-width:650px){.page{width:min(100% - 20px,1480px)}.hero{padding-top:34px}.principle{align-items:flex-start;flex-direction:column;gap:4px}.summary{grid-template-columns:repeat(2,1fr)}.toolbar{grid-template-columns:1fr}.grade-filter,.result-count{grid-row:auto}.case-body{padding:17px}.case-title-row,.software-grid,.flow-grid{display:block}.scene-summary{text-align:left;max-width:none;margin-top:8px}.software-box,.flow{margin-top:8px}.hardware-list{grid-template-columns:1fr}footer{display:block}}
    @media print{.toolbar-wrap{display:none}.cards{grid-template-columns:1fr}.case{break-inside:avoid;box-shadow:none}.hero{padding-top:20px}}
  </style>
</head>
<body class="webpage">
  <header class="hero">
    <div class="page">
      <div class="eyebrow">Smart Canteen Case Atlas · 2026</div>
      <h1>28 个项目，一图理解场景，一表追溯证据</h1>
      <p class="hero-copy">面向销售、渠道与交付团队的智慧食堂案例导航。先看人、场景与主流程，再看已验证的软件、硬件数量口径和仍未闭环的证据。</p>
      <div class="principle"><strong>图负责理解，表负责证据</strong><span>场景图不替代合同 BOM、出库记录、设备 SN、验收点位和现场照片；A/B/C/D 表示本图的还原证据等级，不是项目商业价值评分。</span></div>
      <div class="summary" id="summary"></div>
    </div>
  </header>
  <div class="toolbar-wrap">
    <div class="page toolbar">
      <input class="field" id="search" type="search" placeholder="搜索项目名、场景、人群或能力…" aria-label="搜索项目">
      <select class="field" id="oneToOne" aria-label="按1比1状态筛选"><option value="">全部 1:1 状态</option><option value="部分">部分可1:1</option><option value="否">不可1:1</option></select>
      <div class="grade-filter" id="gradeFilter" aria-label="按证据等级筛选"></div>
      <div class="result-count" id="resultCount"></div>
    </div>
  </div>
  <main class="page">
    <div class="cards" id="cards"></div>
  </main>
  <footer class="page">
    <span>本地自包含图册 · 图片使用项目目录相对路径 · 不加载外部字体、CSS 或 JavaScript</span>
    <span>证据底表：project-image-selection.csv / project-case-base.csv / people · software · hardware detail</span>
  </footer>
  <script>
    const PROJECTS = '''


HTML_TAIL = r''';
    const state = {query:"", grade:"ALL", oneToOne:""};
    const esc = value => String(value ?? "").replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
    const gradeClass = grade => `grade-${esc(grade)}`;
    const oneClass = value => value === "部分" ? "one-part" : value === "是" ? "one-yes" : "one-no";
    const joinText = (items, fallback="待补证") => items && items.length ? items.map(esc).join("、") : fallback;
    function renderSummary(){
      const counts = {A:0,B:0,C:0,D:0}; PROJECTS.forEach(p=>counts[p.grade]++);
      const partial = PROJECTS.filter(p=>p.oneToOne === "部分").length;
      document.getElementById("summary").innerHTML = `
        <div class="summary-card"><b>${PROJECTS.length}</b><span>项目唯一主图</span></div>
        ${["A","B","C","D"].map(g=>`<div class="summary-card"><b>${counts[g]}</b><span>${g}级图像证据</span></div>`).join("")}
        <div class="summary-card"><b>${partial}</b><span>部分可按1:1表达</span></div>`;
    }
    function renderGradeButtons(){
      document.getElementById("gradeFilter").innerHTML = ["ALL","A","B","C","D"].map(g=>`<button type="button" data-grade="${g}" class="${state.grade===g?'active':''}">${g==='ALL'?'全部':g}</button>`).join("");
      document.querySelectorAll("[data-grade]").forEach(btn=>btn.addEventListener("click",()=>{state.grade=btn.dataset.grade;renderGradeButtons();renderCards()}));
    }
    function searchable(p){return [p.id,p.name,p.shortName,p.scene,p.sceneSummary,p.people.join(" "),p.problems,p.actualSoftware.join(" "),p.suggestedSoftware.join(" "),p.hardwareCountBasis].join(" ").toLowerCase()}
    function card(p){
      const metrics = p.metrics.length ? `<div class="metrics">${p.metrics.map(m=>`<div class="metric"><b>${esc(m.value)}</b><span>${esc(m.label)} · ${esc(m.unit)}</span></div>`).join("")}</div>` : "";
      const hardware = p.hardware.length ? `<ul class="hardware-list">${p.hardware.map(h=>`<li><b>${esc(h.name)} · ${esc(h.quantity)}</b>${esc(h.model || '型号待补证')} · ${esc(h.grade || '等级待补')}</li>`).join("")}</ul>` : "";
      const correction = p.correction ? `<div class="correction"><strong>证据纠偏</strong>${esc(p.correction)}</div>` : "";
      return `<article class="case" data-id="${esc(p.id)}">
        <div class="media"><img src="${esc(p.image)}" alt="${esc(p.name)}项目场景图" loading="lazy">
          <div class="badges"><span class="badge ${gradeClass(p.grade)}">${esc(p.grade)}级证据</span><span class="badge ${oneClass(p.oneToOne)}">1:1 · ${esc(p.oneToOne)}</span></div>
          <div class="image-status">${esc(p.imageStatus)}</div>
        </div>
        <div class="case-body">
          <div class="case-title-row"><div><div class="project-id">${esc(p.id)}</div><h2>${esc(p.name)}</h2><div class="scene">${esc(p.scene)}</div></div><div class="scene-summary">${esc(p.sceneSummary)}</div></div>
          ${correction}
          <div class="section"><div class="section-title">服务人群与已观察规模</div><div class="tags">${p.people.map(x=>`<span class="tag">${esc(x)}</span>`).join("")}</div>${metrics}</div>
          <div class="section"><div class="section-title">软件：事实与建议分开看</div><div class="software-grid"><div class="software-box actual"><h3>实际软件 / 运行观察</h3><p>${joinText(p.actualSoftware,esc(p.actualSoftwareFallback || '待补证'))}</p></div><div class="software-box suggested"><h3>场景建议软件</h3><p>${joinText(p.suggestedSoftware)}</p></div></div></div>
          <div class="section"><div class="section-title">硬件与数量口径</div><div class="hardware-basis"><strong>数量证据：</strong>${esc(p.hardwareCountBasis || '待补证')}</div>${hardware}</div>
          <div class="section"><div class="section-title">主流程</div><div class="flow-grid"><div class="flow front">${esc(p.frontFlow)}</div><div class="flow back">${esc(p.kitchenFlow)}</div></div></div>
          <div class="blocker"><strong>正式出图阻断</strong>${esc(p.blocker)}</div>
          <details><summary>查看问题、能力边界与证据来源</summary><p class="detail-copy"><b>场景问题：</b>${esc(p.problems)}</p><p class="detail-copy"><b>验证边界：</b>${esc(p.boundary)}</p><div class="detail-meta">源证据表：${esc(p.sourceTable)}</div></details>
        </div>
      </article>`;
    }
    function renderCards(){
      const q=state.query.trim().toLowerCase();
      const filtered=PROJECTS.filter(p=>(!q||searchable(p).includes(q))&&(state.grade==='ALL'||p.grade===state.grade)&&(!state.oneToOne||p.oneToOne===state.oneToOne));
      document.getElementById("cards").innerHTML=filtered.length?filtered.map(card).join(""):`<div class="empty">没有符合当前条件的项目。请调整项目名、场景、等级或 1:1 筛选。</div>`;
      document.getElementById("resultCount").textContent=`显示 ${filtered.length} / ${PROJECTS.length} 个项目`;
    }
    document.getElementById("search").addEventListener("input",e=>{state.query=e.target.value;renderCards()});
    document.getElementById("oneToOne").addEventListener("change",e=>{state.oneToOne=e.target.value;renderCards()});
    renderSummary(); renderGradeButtons(); renderCards();
  </script>
</body>
</html>
'''


def main() -> None:
    projects = build_projects()
    payload = json.dumps(projects, ensure_ascii=False, separators=(",", ":")).replace("</", "<\\/")
    OUTPUT.parent.mkdir(parents=True, exist_ok=True)
    OUTPUT.write_text(HTML_HEAD + payload + HTML_TAIL, encoding="utf-8")
    print(f"Built {OUTPUT} with {len(projects)} unique project cards")


if __name__ == "__main__":
    main()
