#!/usr/bin/env -S java --source 25

/// Installs airails.dev skills from the cloned (or downloaded) repository into
/// the skill directories of supported AI agents. Run from the repository root:
/// `./installSkills`. Based on https://github.com/adambien/zeeds.
String name = MethodHandles.lookup().lookupClass().getName();
String version = "2026-06-21.1";

List<Path> targets(Path home) {
    return List.of(
            home.resolve(".claude/skills"),
            home.resolve(".vibe/skills"),
            home.resolve(".kiro/skills"),
            home.resolve(".copilot/skills"),
            home.resolve(".agents/skills"), // codex
            home.resolve(".config/goose/skills")
    );
}

void main(String... args) throws Exception {
    IO.println(name + " " + version);
    var home = Path.of(System.getProperty("user.home"));
    switch (args.length == 0 ? "" : args[0]) {
        case "-version" -> {}
        case "-h" -> help();
        case "-l" -> listSkills(home);
        case "-d" -> deleteSkill(home, args.length > 1 ? args[1] : null);
        default -> installSkills(home);
    }
}

void help() {
    IO.println("""
            Usage: %1$s            install all skills
                   %1$s -l         list available and installed skills
                   %1$s -d <name>  delete a skill by folder name
                   %1$s -h         show this help
                   %1$s -version   show version
            """.formatted(name));
}

void installSkills(Path home) throws IOException {
    var source = Path.of("").toAbsolutePath();
    var skills = findSkillDirs(source);
    var skillNames = skills.stream().map(this::skillName).collect(Collectors.joining(", "));
    IO.println("%s: %d skill(s) in %s: %s".formatted(name, skills.size(), source, skillNames));
    var console = requireConsole();
    for (var target : targets(home)) {
        if (!isYes(console.readLine("Install to %s? [y/N] ", target))) {
            IO.println("  skipped");
            continue;
        }
        for (var skill : skills) {
            var dest = target.resolve(skillName(skill));
            copySkill(skill, dest);
            IO.println("  %s -> %s".formatted(skillName(skill), dest));
        }
    }
    IO.println(name + ": done");
}

void listSkills(Path home) throws IOException {
    IO.println(name + ": available");
    for (var skill : findSkillDirs(Path.of("").toAbsolutePath())) {
        IO.println("  " + skillName(skill));
    }
    IO.println(name + ": installed");
    for (var target : targets(home)) {
        var skills = findSkillDirs(target);
        if (skills.isEmpty()) {
            continue;
        }
        IO.println("  " + target);
        for (var skill : skills) {
            IO.println("    " + skillName(skill));
        }
    }
}

void deleteSkill(Path home, String skill) throws IOException {
    if (skill == null) {
        System.err.println(name + ": -d requires a skill name");
        System.exit(1);
    }
    var console = requireConsole();
    var found = false;
    for (var target : targets(home)) {
        var dir = target.resolve(skill);
        if (!Files.exists(dir)) {
            continue;
        }
        found = true;
        if (isYes(console.readLine("  delete %s? [y/N] ", dir))) {
            deleteRecursively(dir);
            IO.println("  deleted");
        } else {
            IO.println("  skipped");
        }
    }
    if (!found) {
        System.err.println("%s: skill '%s' not found in any target".formatted(name, skill));
    }
}

/// Skill directories under `root` — the parent of every `SKILL.md`, skipping
/// dot-directories *below* `root`. Visibility is judged on the relativized path
/// so an agent target whose own prefix carries a dot (`~/.claude/skills`) is not
/// excluded wholesale, only its `.`-prefixed children are.
List<Path> findSkillDirs(Path root) throws IOException {
    if (!Files.exists(root)) {
        return List.of();
    }
    try (var paths = Files.walk(root)) {
        return paths
                .filter(this::isSkillFile)
                .map(Path::getParent)
                .filter(dir -> isVisible(root, dir))
                .sorted()
                .toList();
    }
}

/// Copies the whole skill directory — `SKILL.md` plus any `references/`,
/// `assets/`, or `POWER.md` siblings the skill needs — minus repository noise
/// (`README.md`, dotfiles, `.claude/`). Copying only `SKILL.md` would strip the
/// bundled resources reference-backed skills (sbce, drawio, python-to-java, …)
/// load at runtime.
void copySkill(Path skillDir, Path destDir) throws IOException {
    try (var paths = Files.walk(skillDir)) {
        for (var source : (Iterable<Path>) paths::iterator) {
            var relative = skillDir.relativize(source);
            if (isNoise(relative)) {
                continue;
            }
            var resolved = destDir.resolve(relative);
            if (Files.isDirectory(source)) {
                Files.createDirectories(resolved);
            } else {
                Files.createDirectories(resolved.getParent());
                Files.copy(source, resolved, StandardCopyOption.REPLACE_EXISTING);
            }
        }
    }
}

void deleteRecursively(Path dir) throws IOException {
    try (var paths = Files.walk(dir)) {
        for (var path : (Iterable<Path>) paths.sorted(Comparator.reverseOrder())::iterator) {
            Files.delete(path);
        }
    }
}

Console requireConsole() {
    var console = System.console();
    if (console == null) {
        System.err.println(name + ": interactive terminal required — run ./installSkills directly, not piped");
        System.exit(1);
    }
    return console;
}

boolean isNoise(Path relative) {
    for (var segment : relative) {
        var part = segment.toString();
        if (part.startsWith(".") || part.equals("README.md")) {
            return true;
        }
    }
    return false;
}

boolean isVisible(Path root, Path path) {
    return !root.relativize(path).toString().contains(File.separator + ".");
}

boolean isSkillFile(Path path) {
    return path.getFileName().toString().equals("SKILL.md");
}

boolean isYes(String answer) {
    return answer != null && answer.strip().equalsIgnoreCase("y");
}

String skillName(Path skillDir) {
    return skillDir.getFileName().toString();
}
