#!/usr/bin/env python3
import argparse
import hashlib
import json
import zipfile
from datetime import date
from pathlib import Path, PurePosixPath
from typing import Any


ROOT = Path(__file__).resolve().parent.parent


def display_path(path: Path, root: Path = ROOT) -> str:
    try:
        return str(path.resolve().relative_to(root.resolve()))
    except ValueError:
        return str(path.resolve())


def load_json(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return {}
    return payload if isinstance(payload, dict) else {}


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


def zip_names(zip_path: Path) -> list[str]:
    with zipfile.ZipFile(zip_path) as archive:
        return archive.namelist()


def unsafe_zip_entries(names: list[str]) -> list[str]:
    unsafe = []
    for name in names:
        pure = PurePosixPath(name)
        if pure.is_absolute() or ".." in pure.parts or name.startswith("\\") or (pure.parts and ":" in pure.parts[0]):
            unsafe.append(name)
    return unsafe


def generated_zip_entries(names: list[str]) -> list[str]:
    generated = []
    for name in names:
        parts = PurePosixPath(name).parts
        if ".previews" in parts or "dist" in parts or (len(parts) > 2 and parts[1] == "tests" and any(part.startswith("tmp") for part in parts[2:])):
            generated.append(name)
    return generated


def required_targets(expectations: dict[str, Any], package_dir: Path) -> list[str]:
    targets = expectations.get("required_targets") or []
    if targets:
        return [str(item) for item in targets]
    targets_dir = package_dir / "targets"
    if not targets_dir.exists():
        return []
    return sorted(path.name for path in targets_dir.iterdir() if path.is_dir())


def add_check(checks: list[dict[str, str]], failures: list[str], check_id: str, passed: bool, detail: str) -> None:
    checks.append({"id": check_id, "status": "pass" if passed else "fail", "detail": detail})
    if not passed:
        failures.append(detail)


def package_name(manifest: dict[str, Any], skill_dir: Path) -> str:
    return str(manifest.get("name") or skill_dir.name)


def verify_package(
    skill_dir: Path,
    package_dir: Path,
    expectations: dict[str, Any],
    registry: dict[str, Any],
    require_zip: bool,
    generated_at: str,
) -> dict[str, Any]:
    skill_dir = skill_dir.resolve()
    package_dir = package_dir.resolve()
    checks: list[dict[str, str]] = []
    failures: list[str] = []
    warnings: list[str] = []

    manifest_path = package_dir / "manifest.json"
    manifest = load_json(manifest_path)
    package_root = package_name(manifest, skill_dir)
    add_check(checks, failures, "package-manifest", bool(manifest), f"Package manifest exists: {display_path(manifest_path)}")

    targets = required_targets(expectations, package_dir)
    adapter_paths = []
    for target in targets:
        adapter_path = package_dir / "targets" / target / "adapter.json"
        adapter_paths.append(adapter_path)
        adapter = load_json(adapter_path)
        add_check(checks, failures, f"{target}-adapter", bool(adapter), f"Adapter exists for target: {target}")
        for field in expectations.get("required_fields", []):
            add_check(
                checks,
                failures,
                f"{target}-field-{field}",
                field in adapter,
                f"{target} adapter includes field: {field}",
            )
    required_files_by_target = {
        key[: -len("_required_files")]: value
        for key, value in expectations.items()
        if key.endswith("_required_files")
    }
    for target, required_files in required_files_by_target.items():
        for rel in required_files:
            add_check(checks, failures, f"{target}-file-{rel}", (package_dir / rel).exists(), f"Package contains {rel}")

    archive_path = package_dir / f"{package_root}.zip"
    archive_sha = ""
    archive_entries: list[str] = []
    if archive_path.exists():
        archive_sha = sha256_file(archive_path)
        try:
            archive_entries = zip_names(archive_path)
        except zipfile.BadZipFile:
            add_check(checks, failures, "archive-readable", False, f"Archive is not a readable zip: {display_path(archive_path)}")
        else:
            unsafe_entries = unsafe_zip_entries(archive_entries)
            required_entries = [
                f"{package_root}/SKILL.md",
                f"{package_root}/manifest.json",
                f"{package_root}/agents/interface.yaml",
            ]
            add_check(checks, failures, "archive-safe-paths", not unsafe_entries, "Archive has no absolute or parent-traversal entries")
            for entry in required_entries:
                add_check(checks, failures, f"archive-entry-{entry}", entry in archive_entries, f"Archive contains {entry}")
            generated_entries = generated_zip_entries(archive_entries)
            add_check(checks, failures, "archive-excludes-generated", not generated_entries, "Archive excludes generated dist/, .previews/, and tests/tmp* contents")
    elif require_zip:
        add_check(checks, failures, "archive-present", False, f"Missing required package archive: {display_path(archive_path)}")
    else:
        warnings.append(f"Package archive not found: {display_path(archive_path)}")

    registry_package = registry.get("package", {}) if registry else {}
    if registry_package:
        add_check(checks, failures, "registry-ok", bool(registry.get("ok")), "Registry audit is OK")
        add_check(
            checks,
            failures,
            "registry-name-match",
            registry_package.get("name") == manifest.get("name"),
            "Registry package name matches package manifest",
        )
        add_check(
            checks,
            failures,
            "registry-version-match",
            registry_package.get("version") == manifest.get("version"),
            "Registry package version matches package manifest",
        )
        compatibility = registry_package.get("compatibility", {})
        for target in targets:
            add_check(
                checks,
                failures,
                f"registry-compat-{target}",
                compatibility.get(target) in {"pass", "warn"},
                f"Registry compatibility is reviewable for target: {target}",
            )
    else:
        warnings.append("Registry audit was not supplied; package verification skipped metadata parity checks.")

    report = {
        "ok": not failures,
        "schema_version": "2.0",
        "generated_at": generated_at,
        "skill_dir": display_path(skill_dir),
        "package_dir": display_path(package_dir),
        "summary": {
            "target_count": len(targets),
            "adapter_count": sum(1 for path in adapter_paths if path.exists()),
            "archive_present": archive_path.exists(),
            "archive_sha256": archive_sha,
            "archive_entry_count": len(archive_entries),
            "failure_count": len(failures),
            "warning_count": len(warnings),
        },
        "checks": checks,
        "failures": failures,
        "warnings": warnings,
        "artifacts": {
            "manifest": display_path(manifest_path),
            "archive": display_path(archive_path) if archive_path.exists() else "",
        },
    }
    return report


def render_markdown(report: dict[str, Any]) -> str:
    summary = report["summary"]
    lines = [
        "# Package Verification",
        "",
        f"- OK: `{report['ok']}`",
        f"- Package directory: `{report['package_dir']}`",
        f"- Targets: `{summary['adapter_count']} / {summary['target_count']}` adapters present",
        f"- Archive present: `{summary['archive_present']}`",
        f"- Archive SHA256: `{summary['archive_sha256'] or 'n/a'}`",
        f"- Failures: `{summary['failure_count']}`",
        f"- Warnings: `{summary['warning_count']}`",
        "",
        "## Checks",
        "",
        "| Check | Status | Detail |",
        "| --- | --- | --- |",
    ]
    for item in report["checks"]:
        lines.append(f"| `{item['id']}` | `{item['status']}` | {item['detail']} |")
    lines.extend(["", "## Failures", ""])
    lines.extend([f"- {item}" for item in report["failures"]] or ["- None"])
    lines.extend(["", "## Warnings", ""])
    lines.extend([f"- {item}" for item in report["warnings"]] or ["- None"])
    return "\n".join(lines) + "\n"


def main() -> None:
    parser = argparse.ArgumentParser(description="Verify generated skill package artifacts and archive integrity.")
    parser.add_argument("skill_dir", nargs="?", default=".")
    parser.add_argument("--package-dir", default="dist")
    parser.add_argument("--expectations", default="evals/packaging_expectations.json")
    parser.add_argument("--registry-json", default="reports/registry_audit.json")
    parser.add_argument("--output-json", default="reports/package_verification.json")
    parser.add_argument("--output-md", default="reports/package_verification.md")
    parser.add_argument("--require-zip", action="store_true")
    parser.add_argument("--generated-at", default=str(date.today()))
    args = parser.parse_args()

    skill_dir = Path(args.skill_dir)
    package_dir = Path(args.package_dir)
    if not package_dir.is_absolute():
        package_dir = Path.cwd() / package_dir
    expectations = load_json(Path(args.expectations)) if args.expectations else {}
    registry = load_json(Path(args.registry_json)) if args.registry_json else {}
    report = verify_package(skill_dir, package_dir, expectations, registry, args.require_zip, args.generated_at)

    output_json = Path(args.output_json)
    output_md = Path(args.output_md)
    output_json.parent.mkdir(parents=True, exist_ok=True)
    output_md.parent.mkdir(parents=True, exist_ok=True)
    output_json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    output_md.write_text(render_markdown(report), encoding="utf-8")
    report["artifacts"]["json"] = display_path(output_json)
    report["artifacts"]["markdown"] = display_path(output_md)
    output_json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    print(json.dumps(report, ensure_ascii=False, indent=2))
    raise SystemExit(0 if report["ok"] else 2)


if __name__ == "__main__":
    main()
