import argparse
import json
import shutil
from pathlib import Path


DEFAULT_TARGET_AGENT = "codex"
DEFAULT_CODEX_TARGET_DIR = ".agents/skills"
DEFAULT_OPENCODE_TARGET_DIR = ".opencode/skill"
DEFAULT_CLAUDE_TARGET_DIR = ".claude/skills"
DEFAULT_TARGET_SKILLS_DIR = DEFAULT_CODEX_TARGET_DIR
DEFAULT_ALLOWED_SCOPES = ("downstream_runtime",)

TARGET_AGENT_DIRS = {
    "codex": DEFAULT_CODEX_TARGET_DIR,
    "opencode": DEFAULT_OPENCODE_TARGET_DIR,
    "claude": DEFAULT_CLAUDE_TARGET_DIR,
}


def _skill_dirs(source_skills_dir):
    source = Path(source_skills_dir)
    return sorted(path for path in source.iterdir() if (path / "SKILL.md").is_file())


def _default_manifest_path(source_skills_dir):
    return Path(source_skills_dir).resolve().parent / "templates" / "skill-install-scope.json"


def _load_skill_metadata(manifest_path):
    """Return the full per-skill metadata dict from the manifest."""
    if manifest_path is None or not Path(manifest_path).exists():
        return {}
    data = json.loads(Path(manifest_path).read_text(encoding="utf-8-sig"))
    skills = data.get("skills", {})
    if not isinstance(skills, dict):
        return {}
    return {name: meta for name, meta in skills.items() if isinstance(meta, dict)}


def _load_install_scopes(manifest_path):
    if manifest_path is None or not Path(manifest_path).exists():
        return {}
    data = json.loads(Path(manifest_path).read_text(encoding="utf-8-sig"))
    skills = data.get("skills", {})
    if not isinstance(skills, dict):
        return {}
    return {
        name: metadata.get("install_scope")
        for name, metadata in skills.items()
        if isinstance(metadata, dict)
    }


def _filtered_skill_dirs(source_skills_dir, manifest_path=None, allowed_scopes=DEFAULT_ALLOWED_SCOPES, active_options=None):
    allowed_scopes = set(allowed_scopes)
    scopes = _load_install_scopes(manifest_path)
    metadata = _load_skill_metadata(manifest_path)
    selected = []
    for skill_dir in _skill_dirs(source_skills_dir):
        name = skill_dir.name
        install_scope = scopes.get(name, "downstream_runtime")
        if install_scope not in allowed_scopes:
            continue
        if active_options is not None:
            required = metadata.get(name, {}).get("requires_options")
            if required is None:
                required = ["codex", "opencode"]
            if not isinstance(required, list):
                required = ["codex", "opencode"]
            if not any(opt in active_options for opt in required):
                continue
        selected.append(skill_dir)
    return selected


def _known_skill_names(source_skills_dir, manifest_path=None):
    scopes = _load_install_scopes(manifest_path)
    if scopes:
        return set(scopes)
    return {path.name for path in _skill_dirs(source_skills_dir)}


def _removable_skill_names(target_root, known_skill_names, selected_names):
    if not target_root.exists():
        return []
    return sorted(
        path.name
        for path in target_root.iterdir()
        if path.is_dir() and path.name in known_skill_names and path.name not in selected_names
    )


def _resolve_target_dir(target_agent, target_skills_dir):
    if target_skills_dir is not None:
        return target_skills_dir
    return TARGET_AGENT_DIRS.get(target_agent, DEFAULT_TARGET_SKILLS_DIR)


def _targets_for_agent(target_agent, target_skills_dir, host_agents=None):
    if host_agents:
        return [
            (host, _resolve_target_dir(host, target_skills_dir))
            for host in host_agents
        ]
    if target_agent == "both":
        return [
            ("codex", _resolve_target_dir("codex", target_skills_dir)),
            ("opencode", _resolve_target_dir("opencode", target_skills_dir)),
        ]
    return [(target_agent, _resolve_target_dir(target_agent, target_skills_dir))]


def install_skills(
    repo_root=".",
    source_skills_dir=None,
    target_skills_dir=None,
    manifest_path=None,
    allowed_scopes=DEFAULT_ALLOWED_SCOPES,
    dry_run=False,
    target_agent=DEFAULT_TARGET_AGENT,
    active_options=None,
    host_agents=None,
):
    repo_root = Path(repo_root)
    if source_skills_dir is None:
        source_skills_dir = Path(__file__).resolve().parents[1] / "skills"
    source_skills_dir = Path(source_skills_dir)
    if not source_skills_dir.is_dir():
        raise FileNotFoundError(
            f"Source skills directory not found: {source_skills_dir}. "
            "Pass --source-skills with the path to the PRD Plugin hub skills/ directory."
        )
    if manifest_path is None:
        manifest_path = _default_manifest_path(source_skills_dir)

    if active_options is None:
        active_options = set()
        if host_agents:
            active_options.update(host_agents)
        else:
            if target_agent in ("codex", "both"):
                active_options.add("codex")
            if target_agent in ("opencode", "both"):
                active_options.add("opencode")
            if target_agent == "claude":
                active_options.add("claude")

    skills = _filtered_skill_dirs(
        source_skills_dir, manifest_path, allowed_scopes, active_options
    )
    names = [path.name for path in skills]
    known_skill_names = _known_skill_names(source_skills_dir, manifest_path)

    targets = _targets_for_agent(target_agent, target_skills_dir, host_agents)
    multi = len(targets) > 1
    target_reports = []
    all_installed = []
    all_removed = []

    for agent, rel_target in targets:
        target_root = repo_root / rel_target
        entry = {"agent": agent, "target": str(target_root)}
        if dry_run:
            entry["would_install"] = names
            entry["would_remove"] = _removable_skill_names(target_root, known_skill_names, names)
            target_reports.append(entry)
            continue
        target_root.mkdir(parents=True, exist_ok=True)
        removed_for_target = []
        for destination in sorted(target_root.iterdir()):
            if destination.is_dir() and destination.name in known_skill_names and destination.name not in names:
                shutil.rmtree(destination)
                removed_for_target.append(destination.name)
        installed_for_target = []
        for skill_dir in skills:
            destination = target_root / skill_dir.name
            if destination.exists():
                shutil.rmtree(destination)
            shutil.copytree(skill_dir, destination)
            installed_for_target.append(skill_dir.name)
        entry["installed"] = installed_for_target
        entry["removed"] = removed_for_target
        target_reports.append(entry)
        if multi:
            all_installed.extend(f"{agent}:{name}" for name in installed_for_target)
            all_removed.extend(f"{agent}:{name}" for name in removed_for_target)
        else:
            all_installed.extend(installed_for_target)
            all_removed.extend(removed_for_target)

    primary = target_reports[0] if target_reports else {}
    report = {
        "status": "dry_run" if dry_run else "installed",
        "target_agent": target_agent,
        "targets": target_reports,
        "installed_count": len(all_installed),
    }
    if multi:
        report["installed"] = all_installed
        report["removed"] = all_removed
    else:
        report["target"] = primary.get("target", "")
        report["installed"] = all_installed
        report["removed"] = all_removed
        if dry_run:
            report["would_install"] = primary.get("would_install", [])
            report["would_remove"] = primary.get("would_remove", [])
    return report


def main(argv=None):
    parser = argparse.ArgumentParser(
        description="Install PRD Plugin skills into a repo-local host-agent discovery directory."
    )
    parser.add_argument("--repo-root", default=".", help="Repository root that should receive the skill copies.")
    parser.add_argument(
        "--source-skills",
        default=str(Path(__file__).resolve().parents[1] / "skills"),
        help="Source PRD Plugin skills directory.",
    )
    parser.add_argument(
        "--target-agent",
        choices=("codex", "opencode", "claude", "both"),
        default=DEFAULT_TARGET_AGENT,
        help="Host agent the skills should be installed for. Defaults to codex.",
    )
    parser.add_argument(
        "--target-skills",
        default=None,
        help="Override the default per-agent target directory. Defaults to .agents/skills for codex, .opencode/skill for opencode, and .claude/skills for claude.",
    )
    parser.add_argument(
        "--manifest",
        help="Skill install-scope manifest. Defaults to templates/skill-install-scope.json beside the source skills.",
    )
    parser.add_argument(
        "--include-scope",
        action="append",
        dest="allowed_scopes",
        help="Install skills with this install_scope. May be repeated. Defaults to downstream_runtime.",
    )
    parser.add_argument("--dry-run", action="store_true")
    parser.add_argument("--format", choices=("json",), default="json")
    args = parser.parse_args(argv)

    report = install_skills(
        repo_root=args.repo_root,
        source_skills_dir=args.source_skills,
        target_skills_dir=args.target_skills,
        manifest_path=args.manifest,
        allowed_scopes=args.allowed_scopes or DEFAULT_ALLOWED_SCOPES,
        dry_run=args.dry_run,
        target_agent=args.target_agent,
    )
    print(json.dumps(report, indent=2))
    return 0


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