#!/usr/bin/env python3
"""Build the unified visual layer and evidence-led daily timeline for three AI Native pages."""

from __future__ import annotations

import base64
import csv
import html
import mimetypes
import re
import subprocess
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
PAGES = {
    "AI_AGENT_PROJECT_INTRO_FOR_BEGINNERS.html": {
        "html_version": "V2.3",
        "update_summary": "客户演示账号最小权限 Skill",
        "title": "从建立到现在：每日建设迭代",
        "intro": "给管理者看完整建设轨迹：每天形成了什么成果、推进了多少任务，以及哪些结论仍需证据或人工确认。",
        "monthly_title": "按月份看团队能力怎样形成",
        "monthly_intro": "把整个协作空间按月压缩为可复算的建设证据、能力主题和协作门禁，帮助管理者判断团队是在堆活动，还是在形成可复用能力。",
    },
    "TEAM_AI_NATIVE_COLLABORATION_GUIDE.html": {
        "html_version": "V2.4",
        "update_summary": "客户演示账号 Skill、月度协作复盘、模块导航",
        "title": "从建立到现在：每日协作迭代",
        "intro": "给团队看控制项目如何逐日扩展产品、研发、交付、知识与 Agent 能力，并保留可追溯的任务和提交证据。",
        "monthly_title": "按月份复盘协作进步",
        "monthly_intro": "每月同时检查成果留痕、工作项责任、知识复用和覆盖缺口；没有负责人、截止日期或 Check 证据的事项不计为完成。",
    },
    "AI_PRACTICE_SHARING.html": {
        "html_version": "V2.9",
        "update_summary": "客户演示账号最小权限、生产 SQL 交付执行、灵犀建项",
        "title": "从建立到现在：每日实践沉淀",
        "intro": "给共学者看实践如何逐日进入项目：页面只归纳已留痕的 Git 与任务索引，不把讨论或计划升级成完成事实。",
        "monthly_title": "按月份看实践如何变成团队资产",
        "monthly_intro": "实践分享、线程记录和项目 Skill 分开计数：有记录代表可追溯，经过验证和人工评审后才代表可复用，更不能直接代表全员采用。",
    },
}

STYLE_START = "/* AI_NATIVE_SHOWCASE_STYLE:START */"
STYLE_END = "/* AI_NATIVE_SHOWCASE_STYLE:END */"
TIMELINE_START = "<!-- AI_NATIVE_DAILY_EVOLUTION:START -->"
TIMELINE_END = "<!-- AI_NATIVE_DAILY_EVOLUTION:END -->"
MONTHLY_START = "<!-- AI_NATIVE_MONTHLY_PROGRESS:START -->"
MONTHLY_END = "<!-- AI_NATIVE_MONTHLY_PROGRESS:END -->"
PUBLIC_BASE = "https://img.kxunpt.cn/public/ai-marketing-platform"

MONTH_FOCUS = {
    "2026-04": "建立控制项目和最小治理入口",
    "2026-05": "形成产品、项目、评审和交付资产基础",
    "2026-06": "扩展 Skill 路由、任务索引和验证门禁",
    "2026-07": "建立多人 AI Native 协作与公开展示治理",
    "2026-08": "推进真实任务闭环、能力评测和团队知识留痕",
}

UNIFIED_STYLE = r"""
/* AI_NATIVE_SHOWCASE_STYLE:START */
:root {
  --ink: #17231f;
  --muted: #64716c;
  --line: #d9e2de;
  --soft: #f4f8f6;
  --panel: #ffffff;
  --green: #08745b;
  --green-dark: #075446;
  --green-soft: #e8f5ef;
  --blue: #2f5f9f;
  --amber: #9a6118;
  --red: #a93c35;
  --shadow: 0 18px 52px rgba(21, 63, 50, 0.08);
}
html { scroll-behavior: smooth; }
body {
  margin: 0;
  color: var(--ink);
  background: linear-gradient(180deg, #f1f7f4 0, #ffffff 430px);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  line-height: 1.68;
}
body main {
  width: min(1160px, calc(100% - 40px));
  max-width: none;
  margin: 0 auto;
  padding: 28px 0 72px;
}
a { color: var(--green-dark); text-underline-offset: 3px; }
a:hover { color: var(--green); }
a:focus-visible, button:focus-visible, summary:focus-visible {
  outline: 3px solid rgba(47, 95, 159, 0.30);
  outline-offset: 3px;
}
.topbar {
  position: sticky;
  top: 12px;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  margin: 0 0 22px;
  padding: 12px 16px;
  border: 1px solid rgba(8, 116, 91, 0.16);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.92);
  box-shadow: 0 10px 30px rgba(20, 56, 45, 0.07);
  backdrop-filter: blur(12px);
}
.brand {
  color: var(--green-dark);
  font-size: 13px;
  font-weight: 850;
  letter-spacing: 0.08em;
}
.doc-nav {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0;
}
.doc-nav a {
  padding: 7px 11px;
  border-radius: 9px;
  color: #42514b;
  background: transparent;
  text-decoration: none;
  font-size: 13px;
  font-weight: 700;
}
.doc-nav a:hover,
.doc-nav a[aria-current="page"] {
  color: var(--green-dark);
  background: var(--green-soft);
}
body main > header {
  position: relative;
  overflow: hidden;
  margin: 0 0 4px;
  padding: clamp(34px, 6vw, 62px);
  border: 1px solid rgba(8, 116, 91, 0.20);
  border-radius: 26px;
  background: rgba(255, 255, 255, 0.96);
  box-shadow: var(--shadow);
}
body main > header::after {
  position: absolute;
  right: -110px;
  top: -150px;
  width: 350px;
  height: 350px;
  border: 60px solid rgba(8, 116, 91, 0.065);
  border-radius: 50%;
  content: "";
  pointer-events: none;
}
.eyebrow {
  margin: 0 0 10px;
  color: var(--green);
  font-size: 13px;
  font-weight: 850;
  letter-spacing: 0.13em;
  text-transform: uppercase;
}
body h1 {
  max-width: 850px;
  margin: 0;
  font-size: clamp(38px, 6vw, 64px);
  line-height: 1.06;
  letter-spacing: -0.045em;
}
body .lead {
  max-width: 860px;
  margin: 20px 0 0;
  color: #46564f;
  font-size: 18px;
}
body section,
body .section {
  padding: 36px 0;
  border-bottom: 1px solid var(--line);
}
body h2 {
  margin: 0 0 12px;
  color: var(--ink);
  font-size: 27px;
  line-height: 1.25;
  letter-spacing: -0.02em;
}
body h3 { color: var(--ink); }
.metric, .flow-step, .role-card, .practice-card, .guard-card, .module-card,
.version-strip, .plain-answer, .meta {
  border-color: var(--line);
  border-radius: 16px;
}
.metric, .flow-step, .role-card, .practice-card, .guard-card, .module-card {
  background: var(--panel);
  box-shadow: 0 10px 28px rgba(21, 63, 50, 0.045);
}
.step { grid-template-columns: 36px minmax(0, 1fr); }
.step::before { grid-row: 1 / span 2; }
.step strong, .step span { grid-column: 2; }
.metric .num { color: var(--green); }
.plain-answer {
  border-left-color: var(--green);
  background: var(--green-soft);
}
table { border-collapse: separate; border-spacing: 0; }
th { background: #edf5f1; color: #29483e; }
th, td { border-color: var(--line); }

.evolution-section { position: relative; }
.evolution-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 24px;
}
.evolution-heading p {
  max-width: 760px;
  margin: 8px 0 0;
  color: var(--muted);
}
.evolution-freshness {
  flex: 0 0 auto;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--soft);
  color: #46564f;
  font-size: 12px;
}
.evolution-metrics {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin: 22px 0;
}
.evolution-metric {
  min-height: 108px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 10px 28px rgba(21, 63, 50, 0.045);
}
.evolution-metric strong {
  display: block;
  color: var(--green);
  font-size: 30px;
  line-height: 1.1;
}
.evolution-metric span {
  display: block;
  margin-top: 8px;
  color: var(--muted);
  font-size: 13px;
}
.evolution-source {
  margin: 0 0 18px;
  padding: 14px 16px;
  border-left: 4px solid var(--green);
  border-radius: 0 12px 12px 0;
  background: var(--green-soft);
  color: #315247;
  font-size: 13px;
}
.evolution-month {
  margin: 12px 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: #fff;
}
.evolution-month > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 17px 20px;
  cursor: pointer;
  background: linear-gradient(90deg, #eef7f2, #ffffff);
  list-style: none;
}
.evolution-month > summary::-webkit-details-marker { display: none; }
.evolution-month > summary strong { font-size: 18px; }
.month-counts { color: var(--muted); font-size: 13px; }
.evolution-days { padding: 0 18px 16px; }
.evolution-day {
  display: grid;
  grid-template-columns: 112px 1fr;
  gap: 18px;
  padding: 18px 2px;
  border-top: 1px solid var(--line);
}
.evolution-day:first-child { border-top: 0; }
.day-date time {
  display: block;
  color: var(--green-dark);
  font-weight: 820;
}
.day-date span {
  display: block;
  margin-top: 4px;
  color: var(--muted);
  font-size: 12px;
}
.day-content ul {
  margin: 0;
  padding-left: 18px;
}
.day-content li { margin: 5px 0; }
.day-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}
.day-tag {
  padding: 4px 8px;
  border-radius: 999px;
  background: var(--soft);
  color: #52615b;
  font-size: 11px;
}
.day-more { color: var(--muted); font-size: 12px; }

.monthly-section { position: relative; }
.monthly-heading {
  display: flex;
  align-items: end;
  justify-content: space-between;
  gap: 24px;
}
.monthly-heading p {
  max-width: 760px;
  margin: 8px 0 0;
  color: var(--muted);
}
.asset-stats,
.team-progress-gates {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin: 22px 0;
}
.asset-stat,
.progress-gate {
  min-height: 104px;
  padding: 16px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 10px 28px rgba(21, 63, 50, 0.045);
}
.asset-stat strong,
.progress-gate strong {
  display: block;
  color: var(--green);
  font-size: 28px;
  line-height: 1.1;
}
.asset-stat span,
.progress-gate span {
  display: block;
  margin-top: 8px;
  color: var(--muted);
  font-size: 13px;
}
.progress-gate b {
  display: inline-block;
  margin-top: 8px;
  color: var(--amber);
  font-size: 12px;
}
.progress-gate.is-evidenced b { color: var(--green); }
.monthly-note {
  margin: 0 0 18px;
  padding: 14px 16px;
  border-left: 4px solid var(--blue);
  border-radius: 0 12px 12px 0;
  background: #eef4fb;
  color: #314d70;
  font-size: 13px;
}
.monthly-month {
  margin: 12px 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 18px;
  background: #fff;
}
.monthly-month > summary {
  display: grid;
  grid-template-columns: 112px 1fr auto;
  align-items: center;
  gap: 16px;
  padding: 17px 20px;
  cursor: pointer;
  background: linear-gradient(90deg, #edf4fb, #ffffff);
  list-style: none;
}
.monthly-month > summary::-webkit-details-marker { display: none; }
.monthly-month > summary strong { color: var(--green-dark); font-size: 18px; }
.month-focus { color: #33453e; font-weight: 750; }
.monthly-body {
  display: grid;
  grid-template-columns: minmax(0, 1.3fr) minmax(260px, 0.7fr);
  gap: 18px;
  padding: 18px 20px 20px;
}
.monthly-kpis {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px;
}
.monthly-kpi {
  min-height: 68px;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--soft);
}
.monthly-kpi strong { display: block; color: var(--green-dark); font-size: 19px; }
.monthly-kpi span { color: var(--muted); font-size: 11px; }
.monthly-categories h3 { margin: 0 0 8px; font-size: 15px; }
.monthly-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.monthly-tag {
  padding: 5px 8px;
  border-radius: 999px;
  background: var(--green-soft);
  color: var(--green-dark);
  font-size: 11px;
  font-weight: 700;
}
.monthly-boundary { margin: 10px 0 0; color: var(--muted); font-size: 12px; }

@media (max-width: 820px) {
  body main { width: min(100% - 24px, 1160px); padding-top: 14px; }
  .topbar { position: static; align-items: flex-start; flex-direction: column; }
  body main > header { padding: 30px 22px; border-radius: 20px; }
  body h1 { font-size: clamp(34px, 11vw, 48px); }
  .metric-grid, .flow, .role-grid, .meta-strip, .evolution-metrics,
  .asset-stats, .team-progress-gates {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .evolution-heading, .monthly-heading { align-items: flex-start; flex-direction: column; }
  .evolution-day { grid-template-columns: 1fr; gap: 8px; }
  .monthly-month > summary { grid-template-columns: 1fr; gap: 5px; }
  .monthly-body { grid-template-columns: 1fr; }
  .monthly-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .metric-grid, .flow, .role-grid, .meta-strip, .evolution-metrics,
  .asset-stats, .team-progress-gates {
    grid-template-columns: 1fr;
  }
  .doc-nav { display: grid; width: 100%; grid-template-columns: 1fr; }
  .doc-nav a { width: 100%; }
  body h2 { font-size: 23px; }
  .evolution-month > summary { align-items: flex-start; flex-direction: column; gap: 4px; }
  .monthly-kpis { grid-template-columns: 1fr; }
}
/* AI_NATIVE_SHOWCASE_STYLE:END */
""".strip()


def run_git_log(*, no_merges: bool = False) -> list[dict[str, str]]:
    command = ["git", "log"]
    if no_merges:
        command.append("--no-merges")
    command.extend(["--format=%ad%x1f%h%x1f%s", "--date=short"])
    result = subprocess.run(
        command,
        cwd=ROOT,
        text=True,
        capture_output=True,
        check=True,
    )
    rows: list[dict[str, str]] = []
    for line in result.stdout.splitlines():
        parts = line.split("\x1f", 2)
        if len(parts) == 3:
            rows.append({"date": parts[0], "sha": parts[1], "subject": parts[2]})
    return rows


def tracked_files() -> list[str]:
    result = subprocess.run(
        ["git", "ls-files", "--cached", "--others", "--exclude-standard"],
        cwd=ROOT,
        text=True,
        capture_output=True,
        check=True,
    )
    return [line for line in result.stdout.splitlines() if line]


def tracked_files_matching(*patterns: str) -> list[str]:
    result = subprocess.run(
        ["git", "ls-files", *patterns],
        cwd=ROOT,
        text=True,
        capture_output=True,
        check=True,
    )
    return [line for line in result.stdout.splitlines() if line]


def read_csv_rows(path: Path) -> list[dict[str, str]]:
    if not path.exists():
        return []
    with path.open(encoding="utf-8", newline="") as handle:
        return list(csv.DictReader(handle))


def month_from_date(value: str) -> str:
    match = re.search(r"\d{4}-\d{2}", value)
    return match.group(0) if match else ""


def workspace_evidence() -> dict[str, object]:
    tracked = tracked_files()
    tasks = read_tasks()
    repositories = read_csv_rows(ROOT / "control/zhctproject-repositories.csv")
    members = read_csv_rows(ROOT / "modules/product/product-group-ai-native/control/members.csv")
    work_items = read_csv_rows(ROOT / "modules/product/product-group-ai-native/control/work-items.csv")

    date_directories: dict[str, set[str]] = defaultdict(set)
    thread_records: Counter[str] = Counter()
    daily_archives: Counter[str] = Counter()
    for file_name in tracked:
        parts = Path(file_name).parts
        if file_name.startswith("work/team-learning/thread-records/"):
            month = month_from_date(file_name)
            if month and file_name.endswith(".md"):
                thread_records[month] += 1
        if file_name.startswith("work/team-learning/codex-conversation-archive/daily/"):
            month = month_from_date(file_name)
            if month and file_name.endswith(".md"):
                daily_archives[month] += 1
        if not parts or not parts[0].startswith(("work", "modules", "standards-stack")):
            continue
        for index, part in enumerate(parts[:-1]):
            match = re.match(r"^(\d{4}-\d{2})-\d{2}(?:$|[-_])", part)
            if match:
                date_directories[match.group(1)].add("/".join(parts[: index + 1]))

    practice_shares: Counter[str] = Counter()
    practice_path = ROOT / "standards-stack/ai-engineering/AI_PRACTICE_SHARING.md"
    if practice_path.exists():
        for line in practice_path.read_text(encoding="utf-8").splitlines():
            match = re.match(r"^\|\s*(\d{4}-\d{2})-\d{2}\s*\|", line)
            if match:
                practice_shares[match.group(1)] += 1

    task_updates: Counter[str] = Counter()
    for row in tasks:
        month = month_from_date(row.get("updated_at") or "")
        if month:
            task_updates[month] += 1

    responsible_work_items = 0
    for row in work_items:
        owner = (row.get("decision_owner") or "").strip()
        due_date = (row.get("due_date") or "").strip()
        if owner and "待确认" not in owner and due_date and "待确认" not in due_date:
            responsible_work_items += 1

    active_members = sum(
        1
        for row in members
        if (row.get("join_status") or "").strip().lower() in {"active", "joined"}
    )
    workflow_pattern = re.compile(r"(^|[-_/])(workflow|pipeline|playbook|流程)([-_./]|$)", re.I)
    knowledge_files = 0
    knowledge_documents = 0
    knowledge_map = ROOT / "work_company_knowledge/knowledge-map.md"
    if knowledge_map.exists():
        knowledge_text = knowledge_map.read_text(encoding="utf-8")
        file_match = re.search(r"总文件数：([\d,]+)", knowledge_text)
        document_match = re.search(r"文档类文件数：([\d,]+)", knowledge_text)
        if file_match:
            knowledge_files = int(file_match.group(1).replace(",", ""))
        if document_match:
            knowledge_documents = int(document_match.group(1).replace(",", ""))
    return {
        "tracked": tracked,
        "tasks": tasks,
        "repositories": len(repositories),
        "skills": sum(
            name.startswith("standards-stack/agent-skills/skills/") and name.endswith("/SKILL.md")
            for name in tracked
        ),
        "html_pages": len(tracked_files_matching("*.html")),
        "summaries": sum(name.endswith("/summary.md") or name == "summary.md" for name in tracked),
        "workflows": sum(bool(workflow_pattern.search(name)) for name in tracked),
        "manual_pages": sum(name.startswith("operation-manuals/") and name.endswith(".html") for name in tracked),
        "source_indexes": sum(
            bool(re.search(r"(^|/)(source-index|source_index)[^/]*$", name, flags=re.I))
            for name in tracked
        ),
        "wiki_pages": sum(
            name.startswith("standards-stack/llm-wiki/") and name.endswith(".md")
            for name in tracked
        ),
        "knowledge_files": knowledge_files,
        "knowledge_documents": knowledge_documents,
        "task_updates": task_updates,
        "date_directories": {month: len(paths) for month, paths in date_directories.items()},
        "thread_records": thread_records,
        "daily_archives": daily_archives,
        "practice_shares": practice_shares,
        "responsible_work_items": responsible_work_items,
        "work_item_count": len(work_items),
        "active_members": active_members,
        "member_count": len(members),
    }


def read_tasks() -> list[dict[str, str]]:
    path = ROOT / "control/task-index/tasks.tsv"
    if not path.exists():
        return []
    with path.open(encoding="utf-8", newline="") as handle:
        return list(csv.DictReader(handle, delimiter="\t"))


def task_date(row: dict[str, str]) -> str:
    value = (row.get("updated_at") or "").strip()
    match = re.search(r"\d{4}-\d{2}-\d{2}", value)
    return match.group(0) if match else ""


def classify(text: str) -> list[str]:
    rules = [
        ("产品与方案", r"产品|需求|方案|PRD|原型|市场|用户"),
        ("研发与质量", r"开发|代码|接口|测试|修复|重构|性能|安全"),
        ("交付与运维", r"交付|部署|发布|上线|运维|服务器|手册|视频"),
        ("Agent 与协作", r"Agent|AI |Skill|Prompt|协作|知识|工作流|治理|周报"),
    ]
    tags = [label for label, pattern in rules if re.search(pattern, text, flags=re.I)]
    return tags[:3] or ["项目控制"]


def unique_subjects(commits: list[dict[str, str]], limit: int = 3) -> list[str]:
    values: list[str] = []
    for commit in commits:
        subject = commit["subject"].strip()
        if subject.lower().startswith("merge "):
            continue
        subject = re.sub(r"^(feat|fix|docs|chore|refactor|test)(\([^)]*\))?:\s*", "", subject, flags=re.I)
        if subject and subject not in values:
            values.append(subject)
        if len(values) >= limit:
            break
    if not values and commits:
        values.append("完成分支合并、同步与证据汇总")
    return values


def monthly_progress_html(page_meta: dict[str, str], evidence: dict[str, object]) -> str:
    commits = run_git_log(no_merges=True)
    commits_by_month: dict[str, list[dict[str, str]]] = defaultdict(list)
    commit_days_by_month: dict[str, set[str]] = defaultdict(set)
    category_counts: dict[str, Counter[str]] = defaultdict(Counter)
    for commit in commits:
        month = month_from_date(commit["date"])
        if not month:
            continue
        commits_by_month[month].append(commit)
        commit_days_by_month[month].add(commit["date"])
        category_counts[month].update(classify(commit["subject"]))

    tasks = evidence["tasks"]
    for row in tasks:
        month = month_from_date(row.get("updated_at") or "")
        if month:
            category_counts[month].update(classify((row.get("title") or "").strip()))

    task_updates = evidence["task_updates"]
    date_directories = evidence["date_directories"]
    thread_records = evidence["thread_records"]
    daily_archives = evidence["daily_archives"]
    practice_shares = evidence["practice_shares"]
    months = sorted(
        set(commits_by_month)
        | set(task_updates)
        | set(date_directories)
        | set(thread_records)
        | set(practice_shares),
        reverse=True,
    )
    generated = datetime.now().astimezone().strftime("%Y-%m-%d")
    current_month = generated[:7]

    responsible = evidence["responsible_work_items"]
    work_item_count = evidence["work_item_count"]
    active_members = evidence["active_members"]
    member_count = evidence["member_count"]
    current_threads = thread_records.get(current_month, 0)
    current_shares = practice_shares.get(current_month, 0)
    current_archives = daily_archives.get(current_month, 0)

    body = [
        MONTHLY_START,
        '    <section class="monthly-section" id="monthly-progress">',
        '      <div class="monthly-heading">',
        "        <div>",
        f"          <h2>{html.escape(page_meta['monthly_title'])}</h2>",
        f"          <p>{html.escape(page_meta['monthly_intro'])}</p>",
        "        </div>",
        f'        <span class="evolution-freshness">审计快照 {html.escape(generated)}</span>',
        "      </div>",
        '      <div class="asset-stats" aria-label="当前协作空间资产统计">',
        f'        <div class="asset-stat" data-stat="repositories"><strong>{evidence["repositories"]:,}</strong><span>登记仓库入口</span></div>',
        f'        <div class="asset-stat" data-stat="skills"><strong>{evidence["skills"]:,}</strong><span>项目 Skill</span></div>',
        f'        <div class="asset-stat" data-stat="html-pages"><strong>{evidence["html_pages"]:,}</strong><span>Git 跟踪 HTML</span></div>',
        f'        <div class="asset-stat" data-stat="summaries"><strong>{evidence["summaries"]:,}</strong><span>工作总结</span></div>',
        "      </div>",
        '      <p class="monthly-note"><strong>审计口径：</strong>活动量只证明有可追溯留痕。月度进步还要同时满足责任人、截止日期、验证或评审证据、知识回写和安全边界；缺少 Check 的事项继续标为待闭环。</p>',
        '      <div class="team-progress-gates" aria-label="当前团队进步门禁">',
        f'        <div class="progress-gate"><strong>{responsible}/{work_item_count}</strong><span>工作项具备明确负责人和日期</span><b>其余待人工门禁</b></div>',
        f'        <div class="progress-gate"><strong>{active_members}/{member_count}</strong><span>成员席位完成实名接入</span><b>开放席位不计已接入</b></div>',
        f'        <div class="progress-gate is-evidenced"><strong>{current_threads + current_shares}</strong><span>本月线程记录 + 实践分享</span><b>有留痕，不等于已复用</b></div>',
        f'        <div class="progress-gate{(" is-evidenced" if current_archives else "")}"><strong>{current_archives}</strong><span>本月每日问答归档文件</span><b>{"已有局部样本，未证明全员覆盖" if current_archives else "尚无覆盖证据"}</b></div>',
        "      </div>",
    ]

    for month_index, month in enumerate(months):
        month_commits = commits_by_month.get(month, [])
        month_tasks = task_updates.get(month, 0)
        month_work_dirs = date_directories.get(month, 0)
        month_threads = thread_records.get(month, 0)
        month_shares = practice_shares.get(month, 0)
        focus = MONTH_FOCUS.get(month, "持续扩展可追溯的产品、研发、交付和知识资产")
        open_attr = " open" if month_index == 0 else ""
        body.append(f'      <details class="monthly-month" data-month="{html.escape(month)}"{open_attr}>')
        body.append(
            f'        <summary><strong>{html.escape(month)}</strong><span class="month-focus">{html.escape(focus)}</span>'
            f'<span class="month-counts">{len(commit_days_by_month.get(month, set()))} 个证据日</span></summary>'
        )
        body.append('        <div class="monthly-body">')
        body.append('          <div class="monthly-kpis">')
        for value, label in [
            (len(month_commits), "非合并提交"),
            (month_tasks, "任务更新"),
            (month_work_dirs, "日期型工作包"),
            (month_threads, "线程记录"),
            (month_shares, "实践分享"),
        ]:
            body.append(f'            <div class="monthly-kpi"><strong>{value:,}</strong><span>{label}</span></div>')
        body.append("          </div>")
        body.append('          <div class="monthly-categories">')
        body.append("            <h3>本月留痕主题</h3>")
        body.append('            <div class="monthly-tags">')
        for label, count in category_counts.get(month, Counter()).most_common(4):
            body.append(f'              <span class="monthly-tag">{html.escape(label)} {count}</span>')
        body.append("            </div>")
        body.append('            <p class="monthly-boundary">主题按提交标题和任务标题聚合，只用于导航；业务完成度仍回到对应证据、测试、评审或外部回读。</p>')
        body.append("          </div>")
        body.append("        </div>")
        body.append("      </details>")

    body.extend(["    </section>", MONTHLY_END])
    return "\n".join(body)


def refresh_known_asset_counts(
    text: str, evidence: dict[str, object], page_meta: dict[str, str]
) -> str:
    generated = datetime.now().astimezone().strftime("%Y-%m-%d")
    replacements = [
        (r"这些数字来自 \d{4}-\d{2}-\d{2} 的", f"这些数字来自 {generated} 的"),
        (r"\d[\d,]* 个已登记(?:业务仓库|仓库入口)", f'{evidence["repositories"]:,} 个已登记仓库入口'),
        (r"不代表 \d[\d,]* 个(?:仓库入口|仓库)", f'不代表 {evidence["repositories"]:,} 个仓库入口'),
        (r"\d[\d,]* 个项目 skill", f'{evidence["skills"]:,} 个项目 skill'),
        (r"\d[\d,]* 个项目内 skill", f'{evidence["skills"]:,} 个项目内 skill'),
        (r"\d[\d,]* 个可复用 skill", f'{evidence["skills"]:,} 个可复用 skill'),
        (r"\d[\d,]* 个 skill", f'{evidence["skills"]:,} 个 skill'),
        (r"当前实际 \d[\d,]* 项", f'当前实际 {evidence["skills"]:,} 项'),
        (r"\d[\d,]* 个流程类资产", f'{evidence["workflows"]:,} 个流程类资产'),
        (r"\d[\d,]* 个 HTML 页面", f'{evidence["html_pages"]:,} 个 HTML 页面'),
        (r"\d[\d,]* 个 HTML", f'{evidence["html_pages"]:,} 个 HTML'),
        (r"\d[\d,]* 份工作总结", f'{evidence["summaries"]:,} 份工作总结'),
        (r"\d[\d,]* 份 summary", f'{evidence["summaries"]:,} 份 summary'),
        (r"\d[\d,]* 条任务证据", f'{len(evidence["tasks"]):,} 条任务证据'),
        (r"\d[\d,]* 份团队线程记录", f'{sum(evidence["thread_records"].values()):,} 份团队线程记录'),
        (r"\d[\d,]* 个来源索引", f'{evidence["source_indexes"]:,} 个来源索引'),
        (r"\d[\d,]* 份 LLM Wiki Markdown", f'{evidence["wiki_pages"]:,} 份 LLM Wiki Markdown'),
        (r"\d[\d,]* 份 LLM Wiki", f'{evidence["wiki_pages"]:,} 份 LLM Wiki'),
        (r"\d[\d,]* 条文件索引", f'{evidence["knowledge_files"]:,} 条文件索引'),
        (r"\d[\d,]* 条文档索引", f'{evidence["knowledge_documents"]:,} 条文档索引'),
    ]
    for pattern, replacement in replacements:
        text = re.sub(pattern, replacement, text)

    metric_values = {
        "个可复用 skill": evidence["skills"],
        "个流程类资产": evidence["workflows"],
        "条任务证据": len(evidence["tasks"]),
        "个 HTML 页面": evidence["html_pages"],
        "条文件索引": evidence["knowledge_files"],
        "条文档索引": evidence["knowledge_documents"],
        "个来源索引": evidence["source_indexes"],
        "份工作总结": evidence["summaries"],
    }
    for label, value in metric_values.items():
        pattern = rf'(<span class="num">)[\d,]+(</span>\s*<span class="label">{re.escape(label)}</span>)'
        text = re.sub(pattern, rf"\g<1>{value:,}\g<2>", text)

    html_version = page_meta["html_version"]
    text = re.sub(
        r'(<span>文档版本</span><strong>)[^<]+(</strong>)',
        rf"\g<1>{html_version}\g<2>",
        text,
        count=1,
    )
    text = re.sub(
        r'(<span>更新日期</span><strong>)\d{4}-\d{2}-\d{2}(</strong>)',
        rf"\g<1>{generated}\g<2>",
        text,
        count=1,
    )
    text = re.sub(
        r'(<span>数据快照</span><strong>)\d{4}-\d{2}-\d{2}(</strong>)',
        rf"\g<1>{generated}\g<2>",
        text,
        count=1,
    )
    text = re.sub(
        r'(<span>本次更新</span><strong>)[^<]+(</strong>)',
        rf'\g<1>{page_meta["update_summary"]}\g<2>',
        text,
        count=1,
    )
    text = re.sub(
        r'(<div class="meta"><span>文档版本</span><strong>)[^<]+(</strong></div>)',
        rf"\g<1>{html_version}\g<2>",
        text,
        count=1,
    )
    text = re.sub(
        r'(<div class="meta"><span>更新日期</span><strong>)\d{4}-\d{2}-\d{2}(</strong></div>)',
        rf"\g<1>{generated}\g<2>",
        text,
        count=1,
    )
    text = re.sub(r'更新：\d{4}-\d{2}-\d{2}', f"更新：{generated}", text, count=1)
    text = re.sub(
        r'页面版本 V[\d.]+ · \d{4}-\d{2}-\d{2} · 自包含 HTML',
        f"页面版本 {html_version} · {generated} · 自包含 HTML",
        text,
        count=1,
    )
    text = re.sub(
        r"数据快照：\d{4}-\d{2}-\d{2}。说明：",
        f"数据快照：{generated}。说明：",
        text,
        count=1,
    )
    text = re.sub(
        r"截至 \d{4}-\d{2}-\d{2}，(?=项目内已沉淀)",
        f"截至 {generated}，",
        text,
        count=1,
    )
    text = re.sub(
        r"截至 \d{4}-\d{2}-\d{2}，(?=\d[\d,]* 个 skill)",
        f"截至 {generated}，",
        text,
        count=1,
    )
    return text


def timeline_html(page_meta: dict[str, str]) -> str:
    commits = run_git_log()
    tasks = read_tasks()
    commits_by_day: dict[str, list[dict[str, str]]] = defaultdict(list)
    tasks_by_day: dict[str, list[dict[str, str]]] = defaultdict(list)
    for commit in commits:
        commits_by_day[commit["date"]].append(commit)
    for task in tasks:
        date = task_date(task)
        if date:
            tasks_by_day[date].append(task)

    all_days = sorted(set(commits_by_day) | set(tasks_by_day), reverse=True)
    months: dict[str, list[str]] = defaultdict(list)
    for day in all_days:
        months[day[:7]].append(day)

    first_date = min(all_days) if all_days else "—"
    latest_date = max(all_days) if all_days else "—"
    generated = datetime.now().astimezone().strftime("%Y-%m-%d")
    body: list[str] = [
        TIMELINE_START,
        '    <section class="evolution-section" id="daily-evolution">',
        '      <div class="evolution-heading">',
        "        <div>",
        f"          <h2>{html.escape(page_meta['title'])}</h2>",
        f"          <p>{html.escape(page_meta['intro'])}</p>",
        "        </div>",
        f'        <span class="evolution-freshness">生成于 {html.escape(generated)} · 最新证据日 {html.escape(latest_date)}</span>',
        "      </div>",
        '      <div class="evolution-metrics" aria-label="建设迭代统计">',
        f'        <div class="evolution-metric"><strong>{len(commits):,}</strong><span>Git 提交记录</span></div>',
        f'        <div class="evolution-metric"><strong>{len(commits_by_day)}</strong><span>有提交的建设日</span></div>',
        f'        <div class="evolution-metric"><strong>{len(tasks):,}</strong><span>任务索引记录</span></div>',
        f'        <div class="evolution-metric"><strong>{html.escape(first_date)}</strong><span>项目建立日期</span></div>',
        "      </div>",
        '      <p class="evolution-source"><strong>证据口径：</strong>按 Git 提交日期与任务索引更新时间聚合；只证明项目留痕，不自动证明客户验收、生产上线、经营价值或团队采用。月份可展开查看每个有证据日期的主要迭代。</p>',
    ]

    for month_index, month in enumerate(sorted(months, reverse=True)):
        days = months[month]
        month_commits = sum(len(commits_by_day[d]) for d in days)
        month_tasks = sum(len(tasks_by_day[d]) for d in days)
        open_attr = " open" if month_index == 0 else ""
        body.append(f'      <details class="evolution-month"{open_attr}>')
        body.append(
            f'        <summary><strong>{html.escape(month)} 建设记录</strong>'
            f'<span class="month-counts">{len(days)} 个证据日 · {month_commits} 次提交 · {month_tasks} 条任务更新</span></summary>'
        )
        body.append('        <div class="evolution-days">')
        for day in days:
            day_commits = commits_by_day[day]
            day_tasks = tasks_by_day[day]
            subjects = unique_subjects(day_commits)
            task_titles: list[str] = []
            for row in day_tasks:
                title = (row.get("title") or row.get("task_id") or "").strip()
                if title and title not in task_titles:
                    task_titles.append(title)
                if len(task_titles) >= 2:
                    break
            combined = " ".join(subjects + task_titles)
            tags = classify(combined)
            body.append('          <article class="evolution-day">')
            body.append(
                f'            <div class="day-date"><time datetime="{html.escape(day)}">{html.escape(day)}</time>'
                f'<span>{len(day_commits)} 次提交 · {len(day_tasks)} 条任务</span></div>'
            )
            body.append('            <div class="day-content"><ul>')
            for subject in subjects:
                body.append(f"              <li>{html.escape(subject)}</li>")
            for title in task_titles:
                if title not in subjects:
                    body.append(f"              <li>任务留痕：{html.escape(title)}</li>")
            extra = max(0, len(day_commits) - len(subjects)) + max(0, len(day_tasks) - len(task_titles))
            body.append("            </ul>")
            if extra:
                body.append(f'              <p class="day-more">另有 {extra} 条提交或任务记录已归入当日统计。</p>')
            body.append('              <div class="day-tags">')
            for tag in tags:
                body.append(f'                <span class="day-tag">{html.escape(tag)}</span>')
            body.append("              </div>")
            body.append("            </div>")
            body.append("          </article>")
        body.append("        </div>")
        body.append("      </details>")

    body.extend(["    </section>", TIMELINE_END])
    return "\n".join(body)


def replace_block(text: str, start: str, end: str, replacement: str) -> tuple[str, bool]:
    pattern = re.compile(re.escape(start) + r".*?" + re.escape(end), flags=re.S)
    if pattern.search(text):
        return pattern.sub(replacement, text, count=1), True
    return text, False


def inline_local_images(text: str, page_path: Path) -> str:
    def replace(match: re.Match[str]) -> str:
        source = match.group(2)
        if source.startswith(("data:", "https://", "http://")):
            return match.group(0)
        image_path = (page_path.parent / source).resolve()
        try:
            image_path.relative_to(ROOT)
        except ValueError:
            raise SystemExit(f"image outside project root: {source}")
        if not image_path.is_file():
            raise SystemExit(f"missing local image: {source}")
        mime_type = mimetypes.guess_type(image_path.name)[0] or "application/octet-stream"
        encoded = base64.b64encode(image_path.read_bytes()).decode("ascii")
        return f'{match.group(1)}data:{mime_type};base64,{encoded}{match.group(3)}'

    return re.sub(r'(<img\b[^>]*\bsrc=")([^"]+)(")', replace, text, flags=re.I)


def normalize_page_anchors(text: str, page_path: Path) -> str:
    public_page = f"{PUBLIC_BASE}/{page_path.name}"

    def replace(match: re.Match[str]) -> str:
        target = match.group(2)
        if not target.startswith("#"):
            return match.group(0)
        return f'{match.group(1)}{public_page}{target}{match.group(3)}'

    return re.sub(r'(<a\b[^>]*\bhref=")(#[^"]+)(")', replace, text, flags=re.I)


def render_page(path: Path, page_meta: dict[str, str], evidence: dict[str, object]) -> bool:
    original = path.read_text(encoding="utf-8")
    updated = refresh_known_asset_counts(original, evidence, page_meta)
    updated, had_style = replace_block(updated, STYLE_START, STYLE_END, UNIFIED_STYLE)
    if not had_style:
        updated = updated.replace("</style>", f"\n{UNIFIED_STYLE}\n  </style>", 1)

    monthly = monthly_progress_html(page_meta, evidence)
    updated, had_monthly = replace_block(updated, MONTHLY_START, MONTHLY_END, monthly)
    if not had_monthly:
        timeline_index = updated.find(TIMELINE_START)
        if timeline_index >= 0:
            updated = updated[:timeline_index] + monthly + "\n\n" + updated[timeline_index:]
        else:
            footer_index = updated.find("    <footer")
            if footer_index >= 0:
                updated = updated[:footer_index] + monthly + "\n\n" + updated[footer_index:]
            else:
                updated = updated.replace("  </main>", f"{monthly}\n  </main>", 1)

    timeline = timeline_html(page_meta)
    updated, had_timeline = replace_block(updated, TIMELINE_START, TIMELINE_END, timeline)
    if not had_timeline:
        footer_index = updated.find("    <footer")
        if footer_index >= 0:
            updated = updated[:footer_index] + timeline + "\n\n" + updated[footer_index:]
        else:
            updated = updated.replace("  </main>", f"{timeline}\n  </main>", 1)

    if 'rel="icon"' not in updated:
        updated = updated.replace("</title>", '</title>\n  <link rel="icon" href="data:,">', 1)
    updated = inline_local_images(updated, path)
    updated = normalize_page_anchors(updated, path)

    if updated == original:
        return False
    path.write_text(updated, encoding="utf-8")
    return True


def main() -> int:
    evidence = workspace_evidence()
    changed: list[str] = []
    for relative_path, page_meta in PAGES.items():
        path = ROOT / relative_path
        if not path.exists():
            raise SystemExit(f"missing page: {relative_path}")
        if render_page(path, page_meta, evidence):
            changed.append(relative_path)
    print(
        "AI Native showcase build complete: "
        + (", ".join(changed) if changed else "no changes")
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
