/**
 * CLI command for installing skills from external GitHub repositories
 *
 * Handles: nori-skillsets external <source> [options]
 */
import * as fs from "fs/promises";
import * as path from "path";
import { log } from "@clack/prompts";
import { getCommandNames, } from "../../../cli/commands/cliCommandNames.js";
import { createEmptySkillset } from "../../../cli/commands/new-skillset/newSkillset.js";
import { loadConfig, getActiveSkillset, getDefaultAgents, } from "../../../cli/config.js";
import { AgentRegistry, } from "../../../cli/features/agentRegistry.js";
import { substituteTemplatePaths } from "../../../cli/features/template.js";
import { promptSkillTypes } from "../../../cli/prompts/flows/externalSkillType.js";
import { addSkillToNoriJson, ensureNoriJson, } from "../../../norijson/nori.js";
import { getNoriSkillsetsDir } from "../../../norijson/skillset.js";
import { resolveInstallDir } from "../../../utils/path.js";
import { cloneRepo, cleanupClone, GitCloneError } from "./gitClone.js";
import { discoverSkills } from "./skillDiscovery.js";
import { parseGitHubSource } from "./sourceParser.js";
const COPY_SKIP_DIRS = new Set([".git", "node_modules", "__pycache__"]);
/**
 * Copy a directory recursively, skipping .git, node_modules, and __pycache__
 * @param args - The function arguments
 * @param args.src - Source directory path
 * @param args.dest - Destination directory path
 */
const copyDirRecursive = async (args) => {
    const { src, dest } = args;
    await fs.mkdir(dest, { recursive: true });
    const entries = await fs.readdir(src, { withFileTypes: true });
    for (const entry of entries) {
        if (entry.isDirectory() && COPY_SKIP_DIRS.has(entry.name)) {
            continue;
        }
        const srcPath = path.join(src, entry.name);
        const destPath = path.join(dest, entry.name);
        if (entry.isDirectory()) {
            await copyDirRecursive({ src: srcPath, dest: destPath });
        }
        else {
            await fs.copyFile(srcPath, destPath);
        }
    }
};
/**
 * Apply template substitution to all .md files in a directory recursively
 * @param args - The function arguments
 * @param args.dir - Directory to process
 * @param args.installDir - The .claude directory path for template substitution
 */
const applyTemplateSubstitutionToDir = async (args) => {
    const { dir, installDir } = args;
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const entry of entries) {
        const entryPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            await applyTemplateSubstitutionToDir({ dir: entryPath, installDir });
        }
        else if (entry.name.endsWith(".md")) {
            const content = await fs.readFile(entryPath, "utf-8");
            const substituted = substituteTemplatePaths({ content, installDir });
            await fs.writeFile(entryPath, substituted);
        }
    }
};
/**
 * Install a single discovered skill to the target directories
 * @param args - The function arguments
 * @param args.skill - The discovered skill to install
 * @param args.skillsDir - Path to the live skills directory
 * @param args.installDir - Root installation directory
 * @param args.skillsetsDir - Path to the profiles directory
 * @param args.targetSkillset - Skillset name to update manifests for
 * @param args.sourceUrl - Source repository URL for provenance
 * @param args.ref - Branch/tag that was checked out
 * @param args.subpath - Subpath within the repository
 * @param args.type - The NoriJsonType to write to the skill's nori.json
 * @param args.agents - The resolved default agents to broadcast the skill to
 * @param args.cliName - CLI name for user-facing messages
 */
const installSkill = async (args) => {
    const { skill, skillsDir, installDir, skillsetsDir, targetSkillset, sourceUrl, ref, subpath, type, } = args;
    // Sanitize skill name for directory name
    const skillDirName = skill.name
        .toLowerCase()
        .replace(/[^a-z0-9._]+/g, "-")
        .replace(/^[.\-]+|[.\-]+$/g, "")
        .substring(0, 255) || "unnamed-skill";
    const targetDir = path.join(skillsDir, skillDirName);
    // Check if skill already exists
    let skillExists = false;
    try {
        await fs.access(targetDir);
        skillExists = true;
        log.warn(`Skill "${skill.name}" already exists at ${targetDir}. Overwriting.`);
    }
    catch {
        // Doesn't exist, proceed
    }
    // Remove existing skill directory if it exists
    if (skillExists) {
        await fs.rm(targetDir, { recursive: true, force: true });
    }
    // Copy skill directory to live location
    await copyDirRecursive({ src: skill.dirPath, dest: targetDir });
    // Write nori.json provenance file inside the skill directory
    const noriJsonData = {
        name: skill.name,
        version: "1.0.0",
        type,
        source: sourceUrl,
        ...(ref != null ? { ref } : {}),
        ...(subpath != null ? { subpath } : {}),
        installedAt: new Date().toISOString(),
    };
    await fs.writeFile(path.join(targetDir, "nori.json"), JSON.stringify(noriJsonData, null, 2));
    // Persist raw copy to profile's skills directory
    if (targetSkillset != null) {
        const profileSkillDir = path.join(skillsetsDir, targetSkillset, "skills", skillDirName);
        try {
            await fs.rm(profileSkillDir, { recursive: true, force: true });
            await copyDirRecursive({ src: skill.dirPath, dest: profileSkillDir });
            // Also write nori.json to profile copy
            await fs.writeFile(path.join(profileSkillDir, "nori.json"), JSON.stringify(noriJsonData, null, 2));
        }
        catch (profileCopyErr) {
            const msg = profileCopyErr instanceof Error
                ? profileCopyErr.message
                : String(profileCopyErr);
            log.warn(`Could not persist skill to profile: ${msg}`);
        }
    }
    // Apply template substitution to .md files in the live copy for primary agent
    const agents = args.agents;
    const primaryAgentDir = agents[0].getAgentDir({ installDir });
    await applyTemplateSubstitutionToDir({
        dir: targetDir,
        installDir: primaryAgentDir,
    });
    // Broadcast: copy skill to all other agents' skills directories
    for (const agent of agents.slice(1)) {
        const agentSkillsDir = agent.getSkillsDir({ installDir });
        const agentTargetDir = path.join(agentSkillsDir, skillDirName);
        try {
            await fs.rm(agentTargetDir, { recursive: true, force: true });
            await copyDirRecursive({ src: targetDir, dest: agentTargetDir });
            const agentDir = agent.getAgentDir({ installDir });
            await applyTemplateSubstitutionToDir({
                dir: agentTargetDir,
                installDir: agentDir,
            });
        }
        catch (copyErr) {
            const msg = copyErr instanceof Error ? copyErr.message : String(copyErr);
            log.warn(`Could not copy skill to ${agent.name}: ${msg}`);
        }
    }
    log.success(`Installed skill "${skill.name}" from GitHub`);
    log.info(`Installed to: ${targetDir}`);
    // Update skillset manifests
    if (targetSkillset != null) {
        const skillsetDir = path.join(skillsetsDir, targetSkillset);
        try {
            await addSkillToNoriJson({
                skillsetDir: skillsetDir,
                skillName: skillDirName,
                version: "*",
            });
        }
        catch (noriJsonErr) {
            const msg = noriJsonErr instanceof Error
                ? noriJsonErr.message
                : String(noriJsonErr);
            log.warn(`Could not update nori.json: ${msg}`);
        }
    }
};
/**
 * Download and install skills from an external GitHub repository
 *
 * @param args - The function arguments
 * @param args.source - GitHub URL or shorthand (owner/repo)
 * @param args.cwd - Current working directory
 * @param args.installDir - Optional explicit install directory
 * @param args.skillset - Optional skillset name to add skills to
 * @param args.newSkillset - Optional name for a new skillset to create and install skills into
 * @param args.skill - Optional specific skill name to install
 * @param args.all - Install all discovered skills without prompting
 * @param args.ref - Optional branch/tag to checkout
 * @param args.inline - If true, set all skills to inlined-skill without prompting
 * @param args.extract - If true, set all skills to skill without prompting
 * @param args.cliName - CLI name for user-facing messages
 *
 * @returns Command status
 */
export const externalMain = async (args) => {
    const { source, installDir, skillset, newSkillset, skill, all, ref, cliName, } = args;
    const inlineFlag = args.inline ?? false;
    const extractFlag = args.extract ?? false;
    const commandNames = getCommandNames({ cliName });
    const cliPrefix = cliName ?? "nori-skillsets";
    // 1. Parse source
    const parsed = parseGitHubSource({ source });
    if (parsed == null) {
        log.error(`Invalid source: "${source}".\n\nOnly GitHub repositories are supported. Expected formats:\n  - https://github.com/owner/repo\n  - https://github.com/owner/repo/tree/branch/path\n  - owner/repo\n  - owner/repo@skill-name`);
        return {
            success: false,
            cancelled: false,
            message: `Invalid source: "${source}"`,
        };
    }
    // Use --ref flag if provided, otherwise use ref from URL
    const effectiveRef = ref ?? parsed.ref;
    // 1b. Validate --inline and --extract mutual exclusivity
    if (inlineFlag && extractFlag) {
        log.error(`Cannot use --inline and --extract together. Use one or the other.`);
        return {
            success: false,
            cancelled: false,
            message: "Cannot use --inline and --extract together",
        };
    }
    // 1c. Validate --new and --skillset mutual exclusivity
    if (newSkillset != null && skillset != null) {
        log.error(`Cannot use --new and --skillset together. Use --new to create a new skillset, or --skillset to target an existing one.`);
        return {
            success: false,
            cancelled: false,
            message: "Cannot use --new and --skillset together",
        };
    }
    // 1d. Validate --new name
    if (newSkillset != null && newSkillset.trim().length === 0) {
        log.error(`The --new flag requires a non-empty skillset name.`);
        return {
            success: false,
            cancelled: false,
            message: "The --new flag requires a non-empty skillset name",
        };
    }
    // 2. Resolve install directory from config
    const config = await loadConfig();
    const targetInstallDir = resolveInstallDir({
        cliInstallDir: installDir,
        configInstallDir: config?.installDir,
        agentDirNames: AgentRegistry.getInstance().getAgentDirNames(),
    }).path;
    let targetSkillset = null;
    const skillsetsDir = getNoriSkillsetsDir();
    // 3a. Handle --new: create a new skillset
    if (newSkillset != null) {
        const newSkillsetDir = path.join(skillsetsDir, newSkillset);
        try {
            await fs.access(newSkillsetDir);
            log.error(`Skillset "${newSkillset}" already exists at: ${newSkillsetDir}\n\nChoose a different name or use --skillset to target the existing one.`);
            return {
                success: false,
                cancelled: false,
                message: `Skillset "${newSkillset}" already exists`,
            };
        }
        catch {
            // Expected — directory should not exist
        }
        await createEmptySkillset({ destPath: newSkillsetDir, name: newSkillset });
        targetSkillset = newSkillset;
        log.success(`Created new skillset "${newSkillset}"`);
    }
    else if (skillset != null) {
        const skillsetDir = path.join(skillsetsDir, skillset);
        await ensureNoriJson({ skillsetDir: skillsetDir });
        const skillsetMarker = path.join(skillsetDir, "nori.json");
        try {
            await fs.access(skillsetMarker);
            targetSkillset = skillset;
        }
        catch {
            log.error(`Skillset "${skillset}" not found at: ${skillsetDir}\n\nMake sure the skillset exists and contains a nori.json file.`);
            return {
                success: false,
                cancelled: false,
                message: `Skillset "${skillset}" not found`,
            };
        }
    }
    else if (config != null) {
        const activeSkillset = getActiveSkillset({ config });
        if (activeSkillset != null) {
            const skillsetDir = path.join(skillsetsDir, activeSkillset);
            try {
                await fs.access(skillsetDir);
                targetSkillset = activeSkillset;
            }
            catch {
                // Profile directory doesn't exist - skip manifest update
            }
        }
    }
    // Resolve all default agents for broadcasting
    const defaultAgentNames = getDefaultAgents({ config });
    const defaultAgents = defaultAgentNames.map((name) => AgentRegistry.getInstance().get({ name }));
    const primaryAgent = defaultAgents[0];
    const skillsDir = primaryAgent.getSkillsDir({ installDir: targetInstallDir });
    // Ensure skills directory exists for all agents
    for (const agent of defaultAgents) {
        await fs.mkdir(agent.getSkillsDir({ installDir: targetInstallDir }), {
            recursive: true,
        });
    }
    // 4. Clone repository
    let clonedDir;
    log.info(`Cloning ${parsed.url}...`);
    try {
        clonedDir = await cloneRepo({ url: parsed.url, ref: effectiveRef });
    }
    catch (err) {
        if (err instanceof GitCloneError) {
            log.error(err.message);
        }
        else {
            const msg = err instanceof Error ? err.message : String(err);
            log.error(`Failed to clone repository: ${msg}`);
        }
        return {
            success: false,
            cancelled: false,
            message: `Failed to clone repository: ${source}`,
        };
    }
    try {
        // 5. Discover skills
        const subpath = parsed.subpath;
        let discovered = await discoverSkills({ basePath: clonedDir, subpath });
        // Apply skill filter from @skill syntax
        if (parsed.skillFilter != null) {
            discovered = discovered.filter((s) => s.name.toLowerCase() === parsed.skillFilter.toLowerCase());
            if (discovered.length === 0) {
                log.error(`Skill "${parsed.skillFilter}" not found in repository.`);
                return {
                    success: false,
                    cancelled: false,
                    message: `Skill "${parsed.skillFilter}" not found in repository`,
                };
            }
        }
        if (discovered.length === 0) {
            log.error(`No skills found in ${source}.\n\nA valid skill requires a SKILL.md file with name and description in the YAML frontmatter.`);
            return {
                success: false,
                cancelled: false,
                message: `No skills found in ${source}`,
            };
        }
        // 6. Determine which skills to install
        let skillsToInstall;
        if (skill != null) {
            // Install specific skill by name
            const matched = discovered.filter((s) => s.name.toLowerCase() === skill.toLowerCase());
            if (matched.length === 0) {
                const available = discovered.map((s) => `  - ${s.name}`).join("\n");
                log.error(`Skill "${skill}" not found. Available skills:\n${available}`);
                return {
                    success: false,
                    cancelled: false,
                    message: `Skill "${skill}" not found`,
                };
            }
            skillsToInstall = matched;
        }
        else if (all || discovered.length === 1) {
            // Install all (or single skill found)
            skillsToInstall = discovered;
        }
        else {
            // Multiple skills found, no --skill or --all
            const available = discovered
                .map((s) => `  - ${s.name}: ${s.description}`)
                .join("\n");
            log.error(`Found ${discovered.length} skills in ${source}:\n${available}\n\nSpecify which skill to install:\n  ${cliPrefix} ${commandNames.externalSkill} ${source} --skill <name>\n\nOr install all:\n  ${cliPrefix} ${commandNames.externalSkill} ${source} --all`);
            return {
                success: false,
                cancelled: false,
                message: `Found ${discovered.length} skills but no selection made`,
            };
        }
        // Reconstruct source URL for provenance (without .git suffix for display)
        const sourceUrl = parsed.url.replace(/\.git$/, "");
        // 6b. Resolve skill types (inline vs extract)
        let skillTypeMap;
        if (inlineFlag) {
            skillTypeMap = {};
            for (const s of skillsToInstall) {
                skillTypeMap[s.name] = "inlined-skill";
            }
        }
        else if (extractFlag) {
            skillTypeMap = {};
            for (const s of skillsToInstall) {
                skillTypeMap[s.name] = "skill";
            }
        }
        else {
            const prompted = await promptSkillTypes({
                candidates: skillsToInstall.map((s) => ({ name: s.name })),
            });
            if (prompted == null) {
                return { success: false, cancelled: true, message: "" };
            }
            skillTypeMap = prompted;
        }
        // 7. Install each skill
        for (const skillToInstall of skillsToInstall) {
            await installSkill({
                skill: skillToInstall,
                skillsDir,
                installDir: targetInstallDir,
                skillsetsDir,
                targetSkillset,
                sourceUrl,
                ref: effectiveRef,
                subpath,
                type: skillTypeMap[skillToInstall.name] ?? "skill",
                agents: defaultAgents,
                cliName,
            });
        }
        if (skillsToInstall.length === 1) {
            log.info(`Skill "${skillsToInstall[0].name}" is now available in your Claude Code profile.`);
        }
        else {
            log.info(`${skillsToInstall.length} skills are now available in your Claude Code profile.`);
        }
        if (targetSkillset == null) {
            log.info(`No active skillset - skills not added to any manifest.`);
        }
        const skillNames = skillsToInstall.map((s) => s.name).join(", ");
        return {
            success: true,
            cancelled: false,
            message: skillsToInstall.length === 1
                ? `Installed skill "${skillsToInstall[0].name}" from GitHub`
                : `Installed ${skillsToInstall.length} skills from GitHub: ${skillNames}`,
        };
    }
    finally {
        // 8. Always clean up cloned directory
        await cleanupClone({ dir: clonedDir });
    }
};
/**
 * Register the 'external-skill' command with commander
 *
 * @param args - Configuration arguments
 * @param args.program - Commander program instance
 */
export const registerExternalSkillCommand = (args) => {
    const { program } = args;
    program
        .command("external-skill <source>")
        .description("Install skills from an external GitHub repository")
        .option("--skillset <name>", "Add skill to the specified skillset's manifest (defaults to active skillset)")
        .option("--new <name>", "Create a new skillset and install skills into it")
        .option("--skill <name>", "Install only the named skill from the repository")
        .option("--all", "Install all discovered skills from the repository")
        .option("--ref <ref>", "Branch or tag to checkout")
        .option("--inline", "Set all skills as inlined (bundled in skillset tarball) without prompting")
        .option("--extract", "Set all skills as extracted (independent packages) without prompting")
        .action(async (source, options) => {
        const globalOpts = program.opts();
        await externalMain({
            source,
            installDir: globalOpts.installDir || null,
            skillset: options.skillset || null,
            newSkillset: options.new || null,
            skill: options.skill || null,
            all: options.all || null,
            ref: options.ref || null,
            inline: options.inline || null,
            extract: options.extract || null,
        });
    });
};
//# sourceMappingURL=external.js.map