#!/usr/bin/env python3
import argparse
import json
import shutil
import zipfile
from pathlib import Path, PurePosixPath
import yaml

from compile_skill import compile_target_contract


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


def read_json(path: Path) -> dict:
    if not path.exists():
        return {}
    payload = json.loads(path.read_text(encoding="utf-8"))
    return payload if isinstance(payload, dict) else {}


def read_simple_yaml(path: Path) -> dict:
    return yaml.safe_load(path.read_text(encoding="utf-8")) or {}


def read_frontmatter(skill_md: Path) -> dict:
    if not skill_md.exists():
        raise FileNotFoundError(f"Missing required file: {skill_md}")
    text = skill_md.read_text(encoding="utf-8")
    if not text.startswith("---"):
        return {}
    parts = text.split("---", 2)
    if len(parts) < 3:
        return {}
    return yaml.safe_load(parts[1]) or {}


def read_interface(skill_dir: Path) -> dict:
    path = skill_dir / "agents" / "interface.yaml"
    if not path.exists():
        return {}
    raw = read_simple_yaml(path)
    return raw


def find_skill_ir(skill_dir: Path, name: str) -> tuple[dict, str]:
    candidates = [
        skill_dir / "reports" / "skill-ir.json",
        skill_dir / "skill-ir" / "examples" / f"{name}.json",
        skill_dir / "skill-ir" / "examples" / f"{skill_dir.name}.json",
    ]
    seen = set()
    for path in candidates:
        if path in seen:
            continue
        seen.add(path)
        payload = read_json(path)
        if payload:
            return payload, display_path(path, skill_dir)
    return {}, "frontmatter-fallback"


def package_name_from_manifest(manifest: dict, skill_dir: Path) -> str:
    name = str(manifest.get("name") or "").strip()
    if name:
        return name
    frontmatter = read_frontmatter(skill_dir / "SKILL.md")
    return str(frontmatter.get("name") or skill_dir.name)


def require_fields(payload: dict, fields: list[str], label: str) -> None:
    missing = [field for field in fields if not payload.get(field)]
    if missing:
        raise ValueError(f"Missing required {label} fields: {', '.join(missing)}")


def require_target_degradation(
    degradation: dict,
    targets: list[str],
) -> None:
    missing = [target for target in targets if not degradation.get(target)]
    if missing:
        raise ValueError(f"Missing degradation entries for targets: {', '.join(missing)}")


def count_list(payload: dict, key: str) -> int:
    value = payload.get(key, [])
    return len(value) if isinstance(value, list) else 0


def resource_counts(resources: dict) -> dict:
    return {
        "references": count_list(resources, "references"),
        "scripts": count_list(resources, "scripts"),
        "assets": count_list(resources, "assets"),
        "reports": count_list(resources, "reports"),
    }


def eval_counts(eval_plan: dict) -> dict:
    return {
        "trigger": count_list(eval_plan, "trigger"),
        "output": count_list(eval_plan, "output"),
        "adversarial": count_list(eval_plan, "adversarial"),
        "baseline": 1 if eval_plan.get("baseline") else 0,
    }


def build_semantic_contract(
    *,
    skill_dir: Path,
    platform: str,
    frontmatter: dict,
    interface: dict,
    compatibility: dict,
    manifest: dict,
    ir: dict,
    ir_source: str,
) -> dict:
    trigger_surface = ir.get("trigger_surface", {}) if isinstance(ir.get("trigger_surface"), dict) else {}
    workflow = ir.get("workflow", {}) if isinstance(ir.get("workflow"), dict) else {}
    resources = ir.get("resources", {}) if isinstance(ir.get("resources"), dict) else {}
    eval_plan = ir.get("eval_plan", {}) if isinstance(ir.get("eval_plan"), dict) else {}

    frontmatter_name = str(frontmatter.get("name") or manifest.get("name") or skill_dir.name)
    frontmatter_description = str(frontmatter.get("description") or "")
    ir_name = str(ir.get("name") or "") if ir else ""
    ir_description = str(trigger_surface.get("description") or "") if ir else ""
    name = ir_name or frontmatter_name
    description = ir_description or frontmatter_description
    job = str(ir.get("job_to_be_done") or description)
    targets = ir.get("targets") if isinstance(ir.get("targets"), list) else compatibility.get("adapter_targets", [])
    target_values = [str(item) for item in targets]
    adapter_targets = [str(item) for item in compatibility.get("adapter_targets", [])]

    semantic_contract = {
        "name": name,
        "description": description,
        "job_to_be_done": job,
        "trigger_description": description,
        "should_trigger_count": count_list(trigger_surface, "should_trigger"),
        "should_not_trigger_count": count_list(trigger_surface, "should_not_trigger"),
        "edge_case_count": count_list(trigger_surface, "edge_cases"),
        "workflow_step_count": count_list(workflow, "steps"),
        "decision_point_count": count_list(workflow, "decision_points"),
        "failure_mode_count": count_list(workflow, "failure_modes"),
        "resource_counts": resource_counts(resources),
        "eval_counts": eval_counts(eval_plan),
        "risk": ir.get("risk", {}) if isinstance(ir.get("risk"), dict) else {},
        "governance": ir.get("governance", {}) if isinstance(ir.get("governance"), dict) else {},
        "targets": target_values,
        "source_files_count": count_list(ir, "source_files") if ir else 0,
    }
    alias_declared = (
        platform in {"agent-skills", "vscode"} and "agent-skills-compatible" in target_values
    )
    semantic_parity = {
        "source": "skill-ir" if ir else "frontmatter-fallback",
        "ir_source": ir_source,
        "name_matches_ir": bool(ir) and frontmatter_name == name,
        "description_matches_ir": bool(ir) and frontmatter_description == description,
        "platform_declared_in_ir": platform in target_values
        or (platform == "generic" and "agent-skills-compatible" in target_values)
        or alias_declared,
        "platform_declared_in_interface": platform in adapter_targets,
        "display_name_present": bool(interface.get("display_name")),
        "default_prompt_present": bool(interface.get("default_prompt")),
    }
    return {
        "name": name,
        "description": description,
        "job_to_be_done": job,
        "ir_source": ir_source,
        "ir_schema_version": str(ir.get("schema_version") or "none"),
        "semantic_contract": semantic_contract,
        "semantic_parity": semantic_parity,
    }


def build_manifest(skill_dir: Path, platform: str) -> dict:
    frontmatter = read_frontmatter(skill_dir / "SKILL.md")
    manifest = read_json(skill_dir / "manifest.json")
    interface_doc = read_interface(skill_dir)
    interface = interface_doc.get("interface", {})
    compatibility = interface_doc.get("compatibility", {})
    activation = compatibility.get("activation", {})
    execution = compatibility.get("execution", {})
    trust = compatibility.get("trust", {})
    degradation = compatibility.get("degradation", {})
    require_fields(frontmatter, ["name", "description"], "frontmatter")
    require_fields(interface, ["display_name", "short_description", "default_prompt"], "interface")
    require_fields(compatibility, ["canonical_format", "adapter_targets"], "compatibility")
    require_fields(activation, ["mode"], "compatibility.activation")
    require_fields(execution, ["context", "shell"], "compatibility.execution")
    require_fields(
        trust,
        ["source_tier", "remote_inline_execution", "remote_metadata_policy"],
        "compatibility.trust",
    )
    require_target_degradation(degradation, compatibility.get("adapter_targets", []))
    ir, ir_source = find_skill_ir(skill_dir, str(frontmatter.get("name") or manifest.get("name") or skill_dir.name))
    semantic = build_semantic_contract(
        skill_dir=skill_dir,
        platform=platform,
        frontmatter=frontmatter,
        interface=interface,
        compatibility=compatibility,
        manifest=manifest,
        ir=ir,
        ir_source=ir_source,
    )
    compiled = compile_target_contract(skill_dir, platform)
    if compiled.get("failures"):
        raise ValueError(f"Compiler failed for {platform}: {'; '.join(compiled['failures'])}")
    return {
        "name": semantic["name"],
        "description": semantic["description"],
        "version": manifest.get("version") or frontmatter.get("version", "1.0.0"),
        "platform": platform,
        "skill_root": semantic["name"],
        "job_to_be_done": semantic["job_to_be_done"],
        "ir_source": semantic["ir_source"],
        "ir_schema_version": semantic["ir_schema_version"],
        "semantic_contract": semantic["semantic_contract"],
        "semantic_parity": semantic["semantic_parity"],
        "compiler": compiled["compiler"],
        "compiled_contract": compiled["compiled_contract"],
        "permission_contract": compiled["permission_contract"],
        "target_permission_contract": compiled["target_permission_contract"],
        "target_native_contract": compiled["target_native_contract"],
        "target_transform": compiled["target_transform"],
        "unsupported_features": compiled["unsupported_features"],
        "compiler_warnings": compiled["warnings"],
        "display_name": interface.get("display_name", skill_dir.name),
        "short_description": interface.get("short_description", ""),
        "default_prompt": interface.get("default_prompt", ""),
        "canonical_metadata": "agents/interface.yaml",
        "canonical_format": compatibility.get("canonical_format", "agent-skills"),
        "adapter_targets": compatibility.get("adapter_targets", []),
        "activation_mode": activation.get("mode", "manual"),
        "activation_paths": activation.get("paths", []),
        "execution_context": execution.get("context", "inline"),
        "shell": execution.get("shell", "bash"),
        "trust_level": trust.get("source_tier", "local"),
        "remote_inline_execution": trust.get("remote_inline_execution", "forbid"),
        "remote_metadata_policy": trust.get("remote_metadata_policy", "allow-metadata-only"),
        "degradation_strategy": degradation.get(platform, "neutral-source"),
        "portability_profile": {
            "activation_mode": activation.get("mode", "manual"),
            "activation_paths": activation.get("paths", []),
            "execution_context": execution.get("context", "inline"),
            "shell": execution.get("shell", "bash"),
            "source_tier": trust.get("source_tier", "local"),
            "remote_inline_execution": trust.get("remote_inline_execution", "forbid"),
            "remote_metadata_policy": trust.get("remote_metadata_policy", "allow-metadata-only"),
            "degradation_strategy": degradation.get(platform, "neutral-source"),
        },
    }


PLATFORM_CONTRACTS = {
    "openai": {
        "required_fields": [
            "name",
            "description",
            "version",
            "display_name",
            "short_description",
            "default_prompt",
            "job_to_be_done",
            "ir_source",
            "ir_schema_version",
            "semantic_contract",
            "semantic_parity",
            "canonical_metadata",
            "canonical_format",
            "activation_mode",
            "execution_context",
            "shell",
            "trust_level",
            "remote_inline_execution",
            "degradation_strategy",
            "portability_profile",
            "permission_contract",
            "target_permission_contract",
            "target_native_contract",
        ],
        "required_files": ["targets/openai/adapter.json", "targets/openai/agents/openai.yaml"],
        "field_mapping": {
            "display_name": "interface.display_name",
            "short_description": "interface.short_description",
            "default_prompt": "interface.default_prompt",
            "execution_context": "compatibility.execution.context",
            "shell": "compatibility.execution.shell",
        },
    },
    "claude": {
        "required_fields": [
            "name",
            "description",
            "version",
            "display_name",
            "short_description",
            "default_prompt",
            "job_to_be_done",
            "ir_source",
            "ir_schema_version",
            "semantic_contract",
            "semantic_parity",
            "canonical_metadata",
            "canonical_format",
            "activation_mode",
            "execution_context",
            "shell",
            "trust_level",
            "remote_inline_execution",
            "degradation_strategy",
            "portability_profile",
            "permission_contract",
            "target_permission_contract",
            "target_native_contract",
        ],
        "required_files": ["targets/claude/adapter.json", "targets/claude/README.md"],
        "field_mapping": {
            "display_name": "adapter.display_name",
            "short_description": "adapter.short_description",
            "default_prompt": "adapter.default_prompt",
            "execution_context": "compatibility.execution.context",
            "shell": "compatibility.execution.shell",
        },
    },
    "generic": {
        "required_fields": [
            "name",
            "description",
            "version",
            "display_name",
            "short_description",
            "default_prompt",
            "job_to_be_done",
            "ir_source",
            "ir_schema_version",
            "semantic_contract",
            "semantic_parity",
            "canonical_metadata",
            "canonical_format",
            "activation_mode",
            "execution_context",
            "shell",
            "trust_level",
            "remote_inline_execution",
            "degradation_strategy",
            "portability_profile",
            "permission_contract",
            "target_permission_contract",
            "target_native_contract",
        ],
        "required_files": ["targets/generic/adapter.json"],
        "field_mapping": {
            "display_name": "adapter.display_name",
            "short_description": "adapter.short_description",
            "default_prompt": "adapter.default_prompt",
            "execution_context": "compatibility.execution.context",
            "shell": "compatibility.execution.shell",
        },
    },
    "vscode": {
        "required_fields": [
            "name",
            "description",
            "version",
            "display_name",
            "short_description",
            "default_prompt",
            "job_to_be_done",
            "ir_source",
            "ir_schema_version",
            "semantic_contract",
            "semantic_parity",
            "compiler",
            "compiled_contract",
            "permission_contract",
            "target_permission_contract",
            "target_native_contract",
            "target_transform",
            "canonical_metadata",
            "canonical_format",
            "activation_mode",
            "execution_context",
            "shell",
            "trust_level",
            "remote_inline_execution",
            "degradation_strategy",
            "portability_profile",
        ],
        "required_files": ["targets/vscode/adapter.json", "targets/vscode/README.md"],
        "field_mapping": {
            "name": "SKILL.md::frontmatter.name and folder name",
            "description": "SKILL.md::frontmatter.description",
            "display_name": "agents/interface.yaml::interface.display_name",
            "execution_context": "compatibility.execution.context",
            "permissions": "adapter.target_permission_contract",
        },
    },
}

EXCLUDED_ARCHIVE_PARTS = {".git", ".previews", "__pycache__", ".venv", "venv", "node_modules", "dist"}


def should_skip_archive_path(rel_path: Path) -> bool:
    parts = rel_path.parts
    if any(part in EXCLUDED_ARCHIVE_PARTS for part in parts):
        return True
    if parts == ("reports", "telemetry_events.jsonl"):
        return True
    if parts and parts[0] == "tests" and any(part.startswith("tmp") for part in parts[1:]):
        return True
    return False


def is_relative_to(path: Path, parent: Path) -> bool:
    try:
        path.relative_to(parent)
        return True
    except ValueError:
        return False


def write_yaml_file(path: Path, payload: dict) -> None:
    path.write_text(
        yaml.safe_dump(payload, sort_keys=False, allow_unicode=True),
        encoding="utf-8",
    )


def safe_output_dir(skill_dir: Path, requested_output_dir: Path, cwd: Path) -> Path:
    skill_dir = skill_dir.resolve()
    cwd = cwd.resolve()
    requested_output_dir = requested_output_dir.expanduser()
    candidate = requested_output_dir if requested_output_dir.is_absolute() else cwd / requested_output_dir
    if candidate.is_symlink():
        raise ValueError(f"Refusing symlink output directory: {candidate}")
    out_dir = candidate.resolve()
    home = Path.home().resolve()
    filesystem_root = Path(out_dir.anchor).resolve()

    dangerous_exact = {filesystem_root, home, cwd, skill_dir, skill_dir.parent.resolve()}
    if out_dir in dangerous_exact or out_dir in skill_dir.parents:
        raise ValueError(f"Refusing dangerous output directory: {out_dir}")
    if out_dir.exists() and not out_dir.is_dir():
        raise ValueError(f"Output path exists but is not a directory: {out_dir}")
    if not (is_relative_to(out_dir, cwd) or is_relative_to(out_dir, skill_dir)):
        raise ValueError(f"Output directory must stay under the current workspace or skill directory: {out_dir}")
    return out_dir


def write_adapter(skill_dir: Path, out_dir: Path, platform: str) -> Path:
    target_dir = out_dir / "targets" / platform
    target_dir.mkdir(parents=True, exist_ok=True)
    if platform not in PLATFORM_CONTRACTS:
        raise ValueError(f"Unsupported platform: {platform}")
    payload = build_manifest(skill_dir, platform)
    if platform == "openai":
        meta_dir = target_dir / "agents"
        meta_dir.mkdir(parents=True, exist_ok=True)
        write_yaml_file(
            meta_dir / "openai.yaml",
            {
                "interface": {
                    "display_name": payload["display_name"],
                    "short_description": payload["short_description"],
                    "default_prompt": payload["default_prompt"],
                },
                "compatibility": {
                    "canonical_format": payload["canonical_format"],
                    "activation_mode": payload["activation_mode"],
                    "execution_context": payload["execution_context"],
                    "shell": payload["shell"],
                    "trust_level": payload["trust_level"],
                    "remote_inline_execution": payload["remote_inline_execution"],
                    "permission_contract": {
                        "review_required": payload["target_permission_contract"]["review_required"],
                        "declared_capabilities": payload["target_permission_contract"]["declared_capabilities"],
                        "native_enforcement": payload["target_permission_contract"]["native_enforcement"],
                        "representation": payload["target_permission_contract"]["representation"],
                    },
                    "native_contract": {
                        "native_surface": payload["target_native_contract"]["native_surface"],
                        "activation_policy": payload["target_native_contract"]["activation"]["policy"],
                        "resource_strategy": payload["target_native_contract"]["resources"]["strategy"],
                        "permission_enforcement": payload["target_native_contract"]["permissions"]["enforcement"],
                        "review_artifacts": payload["target_native_contract"]["review"]["artifacts"],
                    },
                    "degradation_strategy": payload["degradation_strategy"],
                },
            },
        )
        payload["install_hint"] = f"Use the packaged skill and include targets/openai/agents/openai.yaml when the client expects OpenAI-style interface metadata."
    elif platform == "claude":
        notes = target_dir / "README.md"
        native = payload["target_native_contract"]
        notes.write_text(
            f"# Claude-Compatible Package\n\nUse `{payload['name']}` with its neutral source files. This target does not require vendor metadata by default.\n\n"
            f"Native surface: {native['native_surface']}.\n\n"
            f"Activation: {native['activation']['policy']}\n\n"
            f"Resources: {native['resources']['strategy']}\n\n"
            f"Scripts: {native['scripts']['strategy']}\n\n"
            f"Permission metadata is preserved in `adapter.json` under `target_permission_contract` and `target_native_contract` for reviewer visibility.\n",
            encoding="utf-8",
        )
        payload["install_hint"] = f"Use the packaged skill directly; this target relies on SKILL.md and optional neutral metadata."
    elif platform == "vscode":
        notes = target_dir / "README.md"
        native = payload["target_native_contract"]
        notes.write_text(
            f"# VS Code / Copilot Agent Skills Package\n\n"
            f"Install `{payload['name']}` as a VS Code user or project scoped Agent Skill. Keep the folder name aligned with `SKILL.md` frontmatter name.\n\n"
            f"Native surface: {native['native_surface']}.\n\n"
            f"Activation: {native['activation']['policy']}\n\n"
            f"Resources: {native['resources']['strategy']}\n\n"
            f"Scripts: {native['scripts']['strategy']}\n\n"
            f"Permission model: {payload['target_permission_contract']['permission_model']}. "
            "Review `target_permission_contract`, workspace trust, and `reports/security_trust_report.md` before running scripts.\n\n"
            "This adapter does not perform automatic VS Code installation; it preserves the reviewed source package plus install notes.\n",
            encoding="utf-8",
        )
        payload["install_hint"] = (
            "Install the package as a VS Code user or project scoped Agent Skill; use targets/vscode/README.md for scope and trust notes."
        )
    else:
        payload["install_hint"] = f"Use {payload['name']} as an Agent Skills compatible package."
    path = target_dir / "adapter.json"
    payload["contract"] = PLATFORM_CONTRACTS.get(platform, PLATFORM_CONTRACTS["generic"])
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    return path


def make_zip(skill_dir: Path, out_dir: Path, package_name: str) -> Path:
    zip_path = out_dir / f"{package_name}.zip"
    skill_root = skill_dir.resolve()
    out_root = out_dir.resolve()
    with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
        for path in skill_dir.rglob("*"):
            if path.is_symlink() or not path.is_file():
                continue
            resolved = path.resolve()
            if not is_relative_to(resolved, skill_root):
                continue
            if is_relative_to(resolved, out_root):
                continue
            rel_path = path.relative_to(skill_dir)
            if should_skip_archive_path(rel_path):
                continue
            zf.write(path, arcname=str(PurePosixPath(package_name, *rel_path.parts)))
    return zip_path


def copy_manifest(skill_dir: Path, out_dir: Path) -> tuple[Path, str]:
    manifest_path = out_dir / "manifest.json"
    manifest = build_manifest(skill_dir, "generic")
    manifest_path.write_text(
        json.dumps(manifest, ensure_ascii=False, indent=2),
        encoding="utf-8",
    )
    return manifest_path, package_name_from_manifest(manifest, skill_dir)


def load_expectations(path: Path | None) -> dict:
    if path is None:
        return {}
    return json.loads(path.read_text(encoding="utf-8"))


def validate_exports(out_dir: Path, expectations: dict) -> dict:
    failures = []
    required_targets = expectations.get("required_targets", [])
    required_fields = expectations.get("required_fields", [])
    required_by_target = {
        key[: -len("_required_files")]: value
        for key, value in expectations.items()
        if key.endswith("_required_files")
    }

    for target in required_targets:
        adapter_path = out_dir / "targets" / target / "adapter.json"
        if not adapter_path.exists():
            failures.append(f"missing adapter for target: {target}")
            continue
        payload = json.loads(adapter_path.read_text(encoding="utf-8"))
        for field in required_fields:
            if field not in payload:
                failures.append(f"missing field '{field}' in {adapter_path.relative_to(out_dir)}")
        for rel in required_by_target.get(target, []):
            if not (out_dir / rel).exists():
                failures.append(f"missing file: {rel}")
    return {"ok": not failures, "failures": failures}


def main() -> None:
    parser = argparse.ArgumentParser(description="Generate lightweight cross-platform packaging artifacts.")
    parser.add_argument("skill_dir", help="Path to the skill directory")
    parser.add_argument("--platform", action="append", default=[], help="Target platform: openai, claude, generic, vscode")
    parser.add_argument("--output-dir", default="dist", help="Output directory")
    parser.add_argument("--expectations", help="JSON file describing packaging expectations")
    parser.add_argument("--zip", action="store_true", help="Create a zip package")
    args = parser.parse_args()

    generated = []
    failures = []
    try:
        skill_dir = Path(args.skill_dir).resolve()
        out_dir = safe_output_dir(skill_dir, Path(args.output_dir), Path.cwd())
        if out_dir.exists():
            shutil.rmtree(out_dir)
        out_dir.mkdir(parents=True)
        manifest, package_name = copy_manifest(skill_dir, out_dir)
        generated.append(str(manifest))
        for platform in (args.platform or ["generic"]):
            generated.append(str(write_adapter(skill_dir, out_dir, platform)))
        if args.zip:
            generated.append(str(make_zip(skill_dir, out_dir, package_name)))
    except (FileNotFoundError, ValueError, yaml.YAMLError) as exc:
        failures.append(str(exc))

    expectations = load_expectations(Path(args.expectations).resolve()) if args.expectations else {}
    validation = validate_exports(out_dir, expectations) if expectations and not failures else None
    report = {
        "output_dir": str(out_dir) if "out_dir" in locals() else str(Path(args.output_dir).resolve()),
        "generated": generated,
        "contracts": PLATFORM_CONTRACTS,
        "validation": validation,
        "failure_handling": {
            "missing_required_file": "exit with code 2 when expectations are provided and validation fails",
            "missing_required_field": "exit with code 2 when expectations are provided and validation fails",
            "invalid_yaml_or_frontmatter": "exit with code 2 when parsing fails",
            "unsupported_platform": "exit with code 2 when the platform is not defined in PLATFORM_CONTRACTS",
        },
        "failures": failures,
    }
    print(json.dumps(report, ensure_ascii=False, indent=2))
    if failures or (validation and not validation["ok"]):
        raise SystemExit(2)


if __name__ == "__main__":
    main()
