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

import argparse
import hashlib
import json
import shutil
from datetime import datetime, timezone
from pathlib import Path

WORKSPACE = Path("workspaces/国际站商家AI交付包")
LEGACY_WORKSPACE_PATHS = [
    "01-店铺与产品基础",
    "02-商品发布与视觉素材",
    "03-橱窗与优爆品运营",
    "04-P4P与RFQ获客",
    "05-客户接待与询盘跟进",
    "06-业务主管与团队协同",
    "10-LLM-Wiki知识库与运行框架",
    "20-Skill与指令",
    "30-执行记录与复盘",
    "40-资料来源与业务素材",
    "MVP最小可运行验收单.md",
    "Skill选择与使用说明.md",
    "AccioWork完整落地搭建方案.md",
    "交付包使用入口.html",
    "国际站经营闭环总入口.html",
    "国际站商家AI交付包-开始使用.md",
    "国际站商家AI交付包总架构.md",
    "国际站商家AI交付包索引.md",
    "_legacy",
]
REQUIRED_ROOT_FILES = [
    "manifest.json",
    "README.md",
    "INSTALL.md",
    "FIRST_RUN.md",
    "install_prompt.md",
    "install-macos.command",
    "install-windows.ps1",
    "install-windows-double-click.cmd",
    "validate_install.py",
    "bindings/agent-skill-map.json",
    "bindings/authorization-policy.json",
    "bindings/first-run-routing.json",
    "bindings/skill-tool-map.json",
    "runtime-tools/tool-manifest.json",
    "manuals/客户使用手册.md",
    "manuals/客户使用手册.html",
    "manuals/首跑指令卡.md",
    "manuals/Windows内网ZIP备用安装说明.md",
    "manuals/macOS安全打开说明.md",
    "workspaces/国际站商家AI交付包/AGENT_START_HERE.md",
    "workspaces/国际站商家AI交付包/00-先读这里/工作目录选择说明.md",
    "workspaces/国际站商家AI交付包/00-先读这里/客户首跑口令.md",
    "workspaces/国际站商家AI交付包/00-先读这里/Agent使用地图.md",
    "workspaces/国际站商家AI交付包/交付包规则/AGENTS.md",
    "workspaces/国际站商家AI交付包/交付包规则/SCHEMA.md",
    "workspaces/国际站商家AI交付包/harness/runtime-policy.json",
    "workspaces/国际站商家AI交付包/LLM-Wiki/README.md",
    "workspaces/国际站商家AI交付包/来源资料/README.md",
    "workspaces/国际站商家AI交付包/运行日志/README.md",
    "workspaces/国际站商家AI交付包/验收与复盘/MVP验收清单.md",
    "workspaces/国际站商家AI交付包/skill-supplements/README.md",
    "workspaces/国际站商家AI交付包/skill-supplements/skill-reference.md",
    "harness/runtime-policy.json",
    "harness/writeback-policy.json",
    "harness/mvp-acceptance.json",
    "harness/workspace-index.json",
    "harness/run-ledger.jsonl",
    "harness/templates/执行记录模板.md",
    "harness/templates/诊断洞察模板.md",
    "harness/templates/读取确认模板.md",
    "checksums/agents.sha256",
    "checksums/skills.sha256",
    "checksums/package.sha256",
]
DEFAULT_AGENTS = {"AIOS交付管家", "国际站运营增长专家", "询盘成交专家", "商品表达与视觉专家", "知识库优化师", "经营复盘与周报专家"}
OPTIONAL_AGENTS = {"OKKI客户经营专家", "展会获客跟进专家", "供应链与选品专家"}
LOCK_KEYS = {"auto_send_customer_message", "auto_quote_commitment", "auto_backend_change", "auto_p4p_budget_change", "auto_rfq_rights_claim", "auto_crm_writeback", "auto_okki_writeback", "auto_customer_assignment"}
SCRIPT_SUFFIXES = {".py", ".js", ".ts", ".sh", ".ps1", ".sql", ".applescript"}
INTERNAL_MARKERS = [
    "source-" + "index.md",
    "source-" + "map.json",
    "merge-" + "matrix",
    "source-" + "inventory",
    "build-" + "report",
    "real-aw-preinstall-" + "snapshot",
    "debug-temp-" + "files",
    "/Users/" + "a33",
    "Co" + "dex",
    "CO" + "DEX",
]


def sha256(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as fh:
        for chunk in iter(lambda: fh.read(1024 * 1024), b""):
            h.update(chunk)
    return h.hexdigest()


def data_sha256(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def load_json(path: Path):
    return json.loads(path.read_text(encoding="utf-8"))


def package_files(root: Path):
    skip_parts = {"output", "reports", "dist", "installer", "skill-sources", "__pycache__"}
    for path in sorted(root.rglob("*")):
        if not path.is_file():
            continue
        rel = path.relative_to(root)
        if any(part in skip_parts for part in rel.parts):
            continue
        if any(part.startswith(".") for part in rel.parts):
            continue
        if rel.as_posix() in {"build_p0_aw_installable_package.py", "checksums/package.sha256"}:
            continue
        yield rel, path


def installable_rel(rel: Path) -> bool:
    return rel.parts[0] in {"agents", "skills", "bindings", "checksums", "workspaces", "harness", "manuals", "runtime-tools"} or rel.as_posix() in {"manifest.json", "README.md", "INSTALL.md", "FIRST_RUN.md", "install_prompt.md", "install-macos.command", "install-windows.ps1", "install-windows-double-click.cmd", "validate_install.py"}


def runtime_mutable_rel(rel: Path) -> bool:
    return rel.name in {"profile.jsonc", "USER.md", "IDENTITY.md", "audit.jsonl"}


def rendered_install_bytes(rel: Path, src: Path, target: Path) -> bytes:
    data = src.read_bytes()
    if len(rel.parts) >= 3 and rel.parts[0] == "agents" and rel.name == "profile.jsonc":
        text = data.decode("utf-8")
        agent_dir = target / "agents" / rel.parts[1]
        return text.replace("__ACCIO_ACCOUNT_ID__", target.name).replace("__ACCIO_AGENT_DIR__", agent_dir.as_posix()).encode("utf-8")
    return data


def parse_checksum_file(path: Path) -> dict[str, str]:
    result = {}
    for line in path.read_text(encoding="utf-8").splitlines():
        parts = line.strip().split()
        if len(parts) >= 2:
            result[" ".join(parts[1:])] = parts[0]
    return result


def skill_layer(skill: dict) -> str:
    sid = skill.get("id", "")
    owner = skill.get("owner_agent", "")
    if skill.get("default_enabled") and skill.get("lock_policy") == "enabled":
        return "P0"
    if "OKKI" in owner or "okki" in sid.lower():
        return "P1"
    return "P2"


def clean_legacy_workspace(target: Path, dry_run: bool) -> list[str]:
    removed = []
    base = target / WORKSPACE
    for rel in LEGACY_WORKSPACE_PATHS:
        path = base / rel
        if path.exists():
            removed.append(str(WORKSPACE / rel))
            if not dry_run:
                if path.is_dir():
                    shutil.rmtree(path)
                else:
                    path.unlink()
    return removed


def text_file(path: Path) -> bool:
    return path.suffix.lower() in {".md", ".txt", ".json", ".jsonc", ".html", ".py", ".ps1", ".command", ".cmd", ".swift", ".nsi", ".yaml", ".yml", ".sh", ".js", ".ts"}


def check_runtime_tools(root: Path, manifest: dict, errors: list[str], evidence: dict) -> None:
    tool_manifest = load_json(root / "runtime-tools" / "tool-manifest.json")
    skill_tool_map = load_json(root / "bindings" / "skill-tool-map.json")
    tool_ids = {tool.get("id") for tool in tool_manifest.get("tools", [])}
    evidence["runtime_tool_count"] = len(tool_ids)
    script_files = []
    for tool in tool_manifest.get("tools", []):
        if not tool.get("id"):
            errors.append("runtime tool missing id")
        for rel in tool.get("files", []):
            path = root / rel
            if not path.exists():
                errors.append(f"runtime tool file missing: {rel}")
            elif path.suffix.lower() in SCRIPT_SUFFIXES:
                script_files.append(rel)
        entry = tool.get("entrypoint")
        if entry and not (root / "runtime-tools" / tool["id"] / entry).exists():
            errors.append(f"runtime tool entrypoint missing: {tool['id']} -> {entry}")
        for locked in tool.get("locked_entrypoints", []):
            if not (root / "runtime-tools" / tool["id"] / locked).exists():
                errors.append(f"runtime locked entrypoint missing: {tool['id']} -> {locked}")
    evidence["script_like_count"] = len(script_files)
    if not script_files:
        errors.append("runtime-tools contains no runnable script-like files")

    skill_ids = {s.get("id") for s in manifest.get("skills", [])}
    for sid, item in skill_tool_map.get("skills", {}).items():
        if sid not in skill_ids:
            errors.append(f"skill-tool-map references missing skill: {sid}")
        for binding in item.get("tools", []):
            tid = binding.get("tool_id")
            if tid not in tool_ids:
                errors.append(f"skill-tool-map references missing tool: {sid} -> {tid}")
            wrapper = binding.get("wrapper")
            if wrapper and not (root / wrapper).exists():
                errors.append(f"skill wrapper missing: {sid} -> {wrapper}")
            tool_root = binding.get("tool_root")
            if tool_root and not (root / tool_root).exists():
                errors.append(f"skill tool_root missing: {sid} -> {tool_root}")


def check_package(root: Path) -> tuple[list[str], list[str], dict]:
    errors, warnings = [], []
    evidence = {}
    for rel in REQUIRED_ROOT_FILES:
        if not (root / rel).exists():
            errors.append(f"missing required file: {rel}")
    for rel in LEGACY_WORKSPACE_PATHS:
        if (root / WORKSPACE / rel).exists():
            errors.append(f"legacy workspace path still packaged: {WORKSPACE / rel}")

    manifest = load_json(root / "manifest.json")
    binding = load_json(root / "bindings/agent-skill-map.json")
    auth = load_json(root / "bindings/authorization-policy.json")
    routing = load_json(root / "bindings/first-run-routing.json")
    check_runtime_tools(root, manifest, errors, evidence)
    evidence["agent_count"] = len(manifest.get("agents", []))
    evidence["skill_count"] = len(manifest.get("skills", []))
    if evidence["agent_count"] != 9:
        errors.append(f"agent count should be 9, got {evidence['agent_count']}")
    if evidence["skill_count"] != 36:
        errors.append(f"skill count should be 36, got {evidence['skill_count']}")

    internal_hits = {}
    all_text_parts = []
    for rel, path in package_files(root):
        if rel.as_posix() == "validate_install.py" or not text_file(path):
            continue
        text = path.read_text(encoding="utf-8", errors="replace")
        all_text_parts.append(text)
        for marker in INTERNAL_MARKERS:
            if marker in text or marker in rel.as_posix():
                internal_hits.setdefault(marker, []).append(rel.as_posix())
    if internal_hits:
        errors.append(f"internal marker found in customer package: {internal_hits}")
    all_text = "\n".join(all_text_parts)
    for bad in ["人工 UI 新建 Team / Agent", "人工 UI 新建", "AccioWork完整落地搭建方案.md", "国际站经营闭环总入口.html"]:
        if bad in all_text:
            errors.append(f"deprecated customer wording/path found: {bad}")
    for marker in ["架构白皮书", "36 个 Skill", "LLM-Wiki", "Harness", "禁用动作"]:
        if marker not in all_text:
            errors.append(f"expected visible marker missing: {marker}")

    agent_names = {a.get("name") for a in manifest.get("agents", [])}
    if DEFAULT_AGENTS - agent_names:
        errors.append(f"missing default agents: {sorted(DEFAULT_AGENTS - agent_names)}")
    if OPTIONAL_AGENTS - agent_names:
        errors.append(f"missing optional agents: {sorted(OPTIONAL_AGENTS - agent_names)}")
    default_agents = [a for a in manifest.get("agents", []) if a.get("default_enabled")]
    if len(default_agents) != 6:
        errors.append(f"default agent count should be 6, got {len(default_agents)}")
    for agent in manifest.get("agents", []):
        if not agent.get("default_enabled") and agent.get("lock_policy") != "locked_until_authorized":
            errors.append(f"optional agent not locked: {agent.get('name')}")

    skill_ids = {s.get("id") for s in manifest.get("skills", [])}
    owner_by_skill = {}
    for aid, item in binding.get("agents", {}).items():
        for sid in item.get("skills", []):
            owner_by_skill.setdefault(sid, []).append(item.get("agent_name", aid))
            if sid not in skill_ids:
                errors.append(f"bound skill missing from manifest: {sid}")
    if len(owner_by_skill) != len(skill_ids):
        errors.append("not every manifest skill appears in agent-skill-map")
    skill_tool_map = load_json(root / "bindings/skill-tool-map.json")
    for skill in manifest.get("skills", []):
        sid = skill["id"]
        if sid not in owner_by_skill:
            errors.append(f"skill has no binding owner: {sid}")
        if sid == "smart-listing-draft-check" and skill.get("lock_policy") != "locked_until_authorized":
            errors.append("smart-listing-draft-check must remain locked_until_authorized")
        expected_layer = skill_layer(skill)
        if expected_layer == "P0" and skill.get("auth_required"):
            errors.append(f"P0 skill unexpectedly auth_required: {sid}")

        skill_dir = root / "skills" / sid
        skill_md = skill_dir / "SKILL.md"
        for rel in ["SKILL.md", "version.json"]:
            if not (skill_dir / rel).exists():
                errors.append(f"missing skill file: {sid}/{rel}")
        if skill_md.exists():
            text = skill_md.read_text(encoding="utf-8", errors="replace")
            required_markers = ["## 触发场景", "## 输入契约", "## 输出契约", "## 执行流程", "## 失败分支", "## STOP 条件", "## 人审边界", "## 工具脚本", "## 样例", "## 模板"]
            for marker in required_markers:
                if marker not in text:
                    errors.append(f"enhanced skill marker missing: {sid} -> {marker}")
            if len(text) < 2000:
                errors.append(f"skill still looks like short template: {sid} ({len(text)} chars)")
            if "M3 脚本/工具增强" in text and not skill_tool_map.get("skills", {}).get(sid, {}).get("tools"):
                errors.append(f"M3 skill has no runtime tool binding: {sid}")
            if skill.get("checksum") != sha256(skill_md):
                errors.append(f"skill checksum mismatch: {sid}")
            if not (skill_dir / "templates" / "input-template.md").exists():
                errors.append(f"skill input template missing: {sid}")

    default_route_agents = {r.get("agent") for r in routing.get("routes", [])}
    for name in OPTIONAL_AGENTS:
        if name in default_route_agents:
            errors.append(f"optional agent appears in default first-run route: {name}")

    for agent in manifest.get("agents", []):
        core = root / "agents" / agent.get("target_dir", agent["id"]) / "agent-core"
        for rel in ["AGENTS.md", "SOUL.md", "IDENTITY.md", "BOOTSTRAP.md", "USER.md", "TOOLS.md", "MEMORY.md", "HEARTBEAT.md", "tool-registry.jsonc", "agent.json"]:
            if not (core / rel).exists():
                errors.append(f"missing agent-core file: {agent.get('name')} {rel}")
        core_text = "\n".join((core / rel).read_text(encoding="utf-8", errors="replace") for rel in ["AGENTS.md", "BOOTSTRAP.md", "TOOLS.md", "MEMORY.md"] if (core / rel).exists())
        for marker in ["AGENT_START_HERE.md", "harness/runtime-policy.json", "读取确认", "写回位置", "写回决策"]:
            if marker not in core_text:
                errors.append(f"agent-core marker missing: {agent.get('name')} -> {marker}")
        for sid in binding.get("agents", {}).get(agent["id"], {}).get("skills", []):
            embedded = core / "skills" / sid / "SKILL.md"
            if not embedded.exists():
                errors.append(f"embedded skill missing: {agent.get('name')} -> {sid}")
            elif "## 工具脚本" not in embedded.read_text(encoding="utf-8", errors="replace"):
                errors.append(f"embedded skill tool section missing: {agent.get('name')} -> {sid}")

    locked = set(auth.get("default_locked", []))
    if LOCK_KEYS - locked:
        errors.append(f"missing authorization locks: {sorted(LOCK_KEYS - locked)}")
    required_sections = set(auth.get("output_required_sections", []))
    for section in ["资料事实", "AI判断", "人工确认项", "写回位置", "写回决策"]:
        if section not in required_sections:
            errors.append(f"missing output section: {section}")
    for rel in (root / "harness" / "workspace-index.json", root / WORKSPACE / "harness" / "runtime-policy.json"):
        try:
            load_json(rel)
        except Exception as exc:
            errors.append(f"harness json invalid: {rel}: {exc}")

    for checksum_rel in ["checksums/skills.sha256", "checksums/agents.sha256", "checksums/package.sha256"]:
        for rel, expected in parse_checksum_file(root / checksum_rel).items():
            path = root / rel
            if not path.exists():
                errors.append(f"checksum target missing in {checksum_rel}: {rel}")
            elif sha256(path) != expected:
                errors.append(f"checksum mismatch in {checksum_rel}: {rel}")

    evidence["routes"] = len(routing.get("routes", []))
    evidence["locks"] = len(locked)
    return errors, warnings, evidence


def copy_package(root: Path, target: Path, dry_run: bool) -> dict:
    planned, copied, errors = [], [], []
    removed_legacy = clean_legacy_workspace(target, dry_run)
    for rel, src in package_files(root):
        if installable_rel(rel):
            planned.append(str(rel))
            if dry_run:
                continue
            dst = target / rel
            dst.parent.mkdir(parents=True, exist_ok=True)
            expected_bytes = rendered_install_bytes(rel, src, target)
            if expected_bytes == src.read_bytes():
                shutil.copy2(src, dst)
            else:
                dst.write_bytes(expected_bytes)
            copied.append(str(rel))
    if not dry_run and not errors:
        (target / "aios-install-report.json").write_text(json.dumps({
            "installed_at": datetime.now(timezone.utc).isoformat(),
            "source_package": str(root),
            "files_copied": len(copied),
            "legacy_workspace_paths_removed": removed_legacy,
            "mode": "additive_aw_compatible_copy_with_aios_legacy_cleanup_and_runtime_tools",
            "overwrite_scope": "aios_package_files_only_no_global_clear",
        }, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return {"planned": planned, "copied": copied, "errors": errors, "removed_legacy": removed_legacy, "target": str(target)}


def verify_target(root: Path, target: Path) -> tuple[list[str], dict]:
    errors = []
    evidence = {"target": str(target), "checked_files": 0, "mutable_files_existence_only": 0}
    for rel in LEGACY_WORKSPACE_PATHS:
        if (target / WORKSPACE / rel).exists():
            errors.append(f"legacy workspace path still installed: {WORKSPACE / rel}")
    for rel, src in package_files(root):
        if installable_rel(rel):
            dst = target / rel
            evidence["checked_files"] += 1
            if not dst.exists():
                errors.append(f"installed file missing: {rel}")
            elif runtime_mutable_rel(rel):
                evidence["mutable_files_existence_only"] += 1
            elif data_sha256(dst.read_bytes()) != data_sha256(rendered_install_bytes(rel, src, target)):
                errors.append(f"installed file checksum mismatch: {rel}")
    run_dir = target / WORKSPACE / "运行日志"
    evidence["run_log_dir"] = str(run_dir)
    if not run_dir.exists():
        errors.append("run log directory missing")
    else:
        probe = run_dir / ".aios-write-test"
        try:
            probe.write_text("write-ok\n", encoding="utf-8")
        finally:
            try:
                probe.unlink()
            except FileNotFoundError:
                pass
    if not (target / "runtime-tools" / "tool-manifest.json").exists():
        errors.append("target runtime-tools missing")
    if not (target / "bindings" / "skill-tool-map.json").exists():
        errors.append("target skill-tool-map missing")
    if not (target / "aios-install-report.json").exists():
        errors.append("missing target install marker: aios-install-report.json")
    return errors, evidence


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--package", default=".")
    ap.add_argument("--mode", choices=["local", "dry-run", "apply", "verify"], default="local")
    ap.add_argument("--target")
    ap.add_argument("--report")
    args = ap.parse_args()
    root = Path(args.package).resolve()
    report = {"mode": args.mode, "package": str(root), "started_at": datetime.now(timezone.utc).isoformat(), "ok": False, "errors": [], "warnings": [], "evidence": {}}
    try:
        errors, warnings, evidence = check_package(root)
        report["errors"].extend(errors)
        report["warnings"].extend(warnings)
        report["evidence"]["package"] = evidence
        if not errors and args.mode in {"dry-run", "apply"}:
            if not args.target:
                report["errors"].append("--target required")
            else:
                result = copy_package(root, Path(args.target).resolve(), dry_run=args.mode == "dry-run")
                report["evidence"]["install"] = result
                report["errors"].extend(result["errors"])
        elif not errors and args.mode == "verify":
            if not args.target:
                report["errors"].append("--target required")
            else:
                target_errors, target_evidence = verify_target(root, Path(args.target).resolve())
                report["errors"].extend(target_errors)
                report["evidence"]["target"] = target_evidence
        report["ok"] = not report["errors"]
    except Exception as exc:
        report["errors"].append(f"exception: {type(exc).__name__}: {exc}")
    report["finished_at"] = datetime.now(timezone.utc).isoformat()
    if args.report:
        path = Path(args.report)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({"ok": report["ok"], "mode": args.mode, "errors": report["errors"], "warnings": report["warnings"]}, ensure_ascii=False))
    return 0 if report["ok"] else 1


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