#!/usr/bin/env python3
"""Inspect built CARBON archives for host and portable package conformance."""

from __future__ import annotations

import argparse
import json
import stat
import zipfile
from pathlib import Path, PurePosixPath


VERSION = "1.32.20"
PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"
MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser()
    parser.add_argument("package_dir", help="Directory produced by scripts/build-packages.sh")
    return parser.parse_args()


def require(condition: bool, message: str) -> None:
    if not condition:
        raise AssertionError(message)


def inspect_archive(
    path: Path,
    required: set[str],
    forbidden_prefixes: tuple[str, ...] = (),
    forbidden_members: set[str] | None = None,
) -> set[str]:
    require(path.is_file(), f"missing package {path.name}")
    forbidden_members = forbidden_members or set()
    with zipfile.ZipFile(path) as archive:
        names = set(archive.namelist())
        for info in archive.infolist():
            name = info.filename
            member = PurePosixPath(name)
            require(not member.is_absolute() and ".." not in member.parts, f"{path.name} has unsafe member {name}")
            require(not name.startswith(forbidden_prefixes), f"{path.name} leaked host or local state: {name}")
            require(name not in forbidden_members, f"{path.name} contains conflicting entry point {name}")
            if stat.S_ISLNK(info.external_attr >> 16):
                target = archive.read(info).decode("utf-8")
                target_path = PurePosixPath(target)
                require(
                    not target_path.is_absolute() and ".." not in target_path.parts,
                    f"{path.name} has escaping symlink {name} -> {target}",
                )
        missing = required - names
        require(not missing, f"{path.name} is missing {sorted(missing)}")
    return names


def read_json(path: Path, member: str) -> dict:
    with zipfile.ZipFile(path) as archive:
        value = json.loads(archive.read(member))
    require(isinstance(value, dict), f"{path.name}:{member} must be an object")
    return value


package_dir = Path(parse_args().package_dir).expanduser().resolve()

openai_path = package_dir / "carbon-openai.zip"
inspect_archive(
    openai_path,
    {
        "carbon/.codex-plugin/plugin.json",
        "carbon/.mcp.json",
        "carbon/skills/carbon/SKILL.md",
        "carbon/server/index.js",
    },
    ("carbon/.carbon/", "carbon/.jank/", "carbon/output/", "carbon/.claude-plugin/", "carbon/.cursor-plugin/"),
    {"carbon/plugin.json", "carbon/mcp.json"},
)
openai = read_json(openai_path, "carbon/.codex-plugin/plugin.json")
require(str(openai.get("version", "")).split("+")[0] == VERSION, "OpenAI package version is wrong")

agent_path = package_dir / "carbon-agent-plugin.zip"
agent_names = inspect_archive(
    agent_path,
    {
        "carbon/plugin.json",
        "carbon/mcp.json",
        "carbon/skills/carbon/SKILL.md",
        "carbon/server/index.js",
    },
    (
        "carbon/.carbon/",
        "carbon/.jank/",
        "carbon/output/",
        "carbon/.codex-plugin/",
        "carbon/.claude-plugin/",
        "carbon/.cursor-plugin/",
    ),
)
agent = read_json(agent_path, "carbon/plugin.json")
agent_mcp = read_json(agent_path, "carbon/mcp.json")
require(agent.get("$schema") == PLUGIN_SCHEMA and agent.get("version") == VERSION, "Agent package manifest is wrong")
require(agent_mcp.get("$schema") == MCP_SCHEMA, "Agent package MCP schema is wrong")
require(len([name for name in agent_names if name.startswith("carbon/skills/") and name.endswith("/SKILL.md")]) == 44, "Agent package must contain 44 skills")

cursor_path = package_dir / "carbon-cursor.zip"
inspect_archive(
    cursor_path,
    {
        "carbon/.cursor-plugin/plugin.json",
        "carbon/mcp.json",
        "carbon/skills/carbon/SKILL.md",
        "carbon/server/index.js",
    },
    ("carbon/.carbon/", "carbon/.jank/", "carbon/output/", "carbon/.codex-plugin/", "carbon/.claude-plugin/"),
    {"carbon/plugin.json"},
)
cursor = read_json(cursor_path, "carbon/.cursor-plugin/plugin.json")
require(cursor.get("version") == VERSION and cursor.get("mcpServers") == "./mcp.json", "Cursor package manifest is wrong")

portable_path = package_dir / "carbon.plugin"
inspect_archive(
    portable_path,
    {"plugin.json", "mcp.json", "skills/carbon/SKILL.md", "server/index.js"},
    (".carbon/", ".jank/", "output/", ".codex-plugin/", ".claude-plugin/", ".cursor-plugin/"),
)
portable = read_json(portable_path, "plugin.json")
require(portable.get("$schema") == PLUGIN_SCHEMA and portable.get("version") == VERSION, "portable .plugin manifest is wrong")

inspect_archive(
    package_dir / "carbon-codex.zip",
    {
        ".agents/plugins/marketplace.json",
        "plugins/carbon/.codex-plugin/plugin.json",
        "plugins/carbon/.mcp.json",
    },
    ("plugins/carbon/.carbon/", "plugins/carbon/.jank/", "plugins/carbon/.claude-plugin/", "plugins/carbon/.cursor-plugin/"),
    {"plugins/carbon/plugin.json", "plugins/carbon/mcp.json"},
)
claude_path = package_dir / "carbon-claude.zip"
inspect_archive(
    claude_path,
    {
        "carbon/.claude-plugin/plugin.json",
        "carbon/.mcp.json",
        "carbon/skills/carbon/SKILL.md",
    },
    ("carbon/.carbon/", "carbon/.jank/", "carbon/.codex-plugin/", "carbon/.cursor-plugin/"),
    {"carbon/plugin.json", "carbon/mcp.json"},
)
claude = read_json(claude_path, "carbon/.claude-plugin/plugin.json")
claude_mcp = read_json(claude_path, "carbon/.mcp.json")
require(claude.get("version") == VERSION, "Claude package version is wrong")
require("mcpServers" not in claude, "Claude package must let the host discover root .mcp.json")
require(set(claude_mcp.get("mcpServers", {})) == {"carbon"}, "Claude package MCP server map is wrong")
require(
    claude_mcp["mcpServers"]["carbon"].get("cwd") == "."
    and claude_mcp["mcpServers"]["carbon"].get("args") == ["./server/index.js"],
    "Claude package MCP entry must resolve from the installed plugin root",
)
inspect_archive(package_dir / "carbon-antigravity.plugin", {"plugin.json", "mcp.json"})
inspect_archive(
    package_dir / "carbon-mcp.zip",
    {"carbon-mcp/bin/carbon-mcp.mjs", "carbon-mcp/server/index.js"},
    (
        "carbon-mcp/.carbon/",
        "carbon-mcp/.jank/",
        "carbon-mcp/.codex-plugin/",
        "carbon-mcp/.claude-plugin/",
        "carbon-mcp/.cursor-plugin/",
    ),
    {"carbon-mcp/plugin.json", "carbon-mcp/mcp.json"},
)

print(
    "PASS: OpenAI, Agent Plugins, Cursor, Codex marketplace, Claude, portable, "
    "Antigravity, and standalone MCP archives are safe and structurally complete."
)
