import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { getSkillsDir, getConfigDir } from "../config/index.js";
import { logger } from "../logger.js";
import { getPackageRoot } from "../utils/version-check.js";
function getSkillsSourceDir() {
    const root = getPackageRoot();
    const candidateDirs = [
        path.join(root, "dist", "builtin-skills"),
        path.join(root, "src", "skills"),
    ];
    for (const dir of candidateDirs) {
        if (!fs.existsSync(dir))
            continue;
        try {
            const entries = fs.readdirSync(dir, { withFileTypes: true });
            const hasAnySkillFolder = entries.some((e) => {
                if (!e.isDirectory())
                    return false;
                return fs.existsSync(path.join(dir, e.name, "SKILL.md"));
            });
            if (!hasAnySkillFolder)
                continue;
        }
        catch {
            continue;
        }
        return dir;
    }
    return null;
}
/**
 * Resolve the catalog source directory (contains optional, non-auto-installed skills).
 * Returns null if not found.
 */
export function getCatalogSourceDir() {
    const sourceDir = getSkillsSourceDir();
    if (!sourceDir)
        return null;
    const catalogDir = path.join(sourceDir, "catalog");
    if (!fs.existsSync(catalogDir))
        return null;
    return catalogDir;
}
/**
 * Skills that have been replaced by MCP tools or otherwise removed.
 * Any matching directories in the user's skills folder are deleted on startup.
 */
const DEPRECATED_SKILLS = ["transcribe-audio"];
/** Root directory where pending skill-update snapshots are written. */
function getPendingUpdatesDir() {
    return path.join(getConfigDir(), "pending-skill-updates");
}
/** Compute SHA-256 hex digest of a string. */
function computeHash(content) {
    return crypto.createHash("sha256").update(content, "utf8").digest("hex");
}
/**
 * Read the local base hash from `.skill-local`.
 * This is the SHA-256 of the local SKILL.md at the time it was last
 * installed, auto-updated, or merged via Learning Mode.
 * Returns null if the file doesn't exist or is unreadable.
 */
function readLocalBaseHash(skillTarget) {
    const file = path.join(skillTarget, ".skill-local");
    if (!fs.existsSync(file))
        return null;
    try {
        const hash = fs.readFileSync(file, "utf8").trim();
        return hash || null;
    }
    catch {
        return null;
    }
}
/**
 * Return the names of skills that have a pending update waiting for agent review.
 * Each name corresponds to a subdirectory under `pending-skill-updates/` containing
 * the full set of new skill files.
 */
export function getPendingSkillUpdates() {
    const dir = getPendingUpdatesDir();
    if (!fs.existsSync(dir))
        return [];
    try {
        return fs
            .readdirSync(dir, { withFileTypes: true })
            .filter((e) => e.isDirectory())
            .map((e) => e.name);
    }
    catch {
        return [];
    }
}
/**
 * Return a map of { relativePath -> content } for all files in a pending skill
 * update, including files in nested subdirectories.
 * Returns null if no pending update directory exists for the given skill name.
 */
export function readPendingSkillUpdate(skillName) {
    const dir = path.join(getPendingUpdatesDir(), skillName);
    if (!fs.existsSync(dir))
        return null;
    try {
        const result = new Map();
        readDirRecursive(dir, "", result);
        return result;
    }
    catch {
        return null;
    }
}
/** Recursively read all files under `root`, building a map of relativePath -> content. */
function readDirRecursive(root, prefix, out) {
    for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
        if (entry.name.startsWith("."))
            continue; // skip internal metadata (.update-meta etc.)
        const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
        const full = path.join(root, entry.name);
        if (entry.isDirectory()) {
            readDirRecursive(full, rel, out);
        }
        else {
            out.set(rel, fs.readFileSync(full, "utf8"));
        }
    }
}
/**
 * Read the source hash from `.update-meta` in a pending skill snapshot.
 * Returns the source SKILL.md hash, or null if not found.
 */
export function readPendingSourceHash(skillName) {
    const file = path.join(getPendingUpdatesDir(), skillName, ".update-meta");
    if (!fs.existsSync(file))
        return null;
    try {
        const hash = fs.readFileSync(file, "utf8").trim();
        return hash || null;
    }
    catch {
        return null;
    }
}
/**
 * Remove a pending skill update entry (call after the agent has processed it).
 */
export function clearPendingSkillUpdate(skillName) {
    const dir = path.join(getPendingUpdatesDir(), skillName);
    try {
        if (fs.existsSync(dir))
            fs.rmSync(dir, { recursive: true, force: true });
    }
    catch {
        /* ignore */
    }
}
/**
 * Remove ALL pending skill updates at once.
 * Used when the learning gate gives up or hits an error.
 */
export function clearAllPendingSkillUpdates() {
    const dir = getPendingUpdatesDir();
    try {
        if (fs.existsSync(dir))
            fs.rmSync(dir, { recursive: true, force: true });
    }
    catch {
        /* ignore */
    }
}
/**
 * Install built-in skills to the active profile's skills directory.
 * Copies from src/skills/ to {profileDir}/.claude/skills/.
 *
 * Three-hash comparison (when force=false):
 *   sourceHash       = hash of SKILL.md from the new VisionClaw version
 *   localBaseHash    = hash stored in .skill-local (last install/update/merge)
 *   localCurrentHash = hash of SKILL.md currently on disk in the profile
 *
 *  - sourceHash == localBaseHash: source already learned, nothing to do.
 *  - sourceHash != localBaseHash && localBaseHash == localCurrentHash:
 *    agent hasn't customised it, safe to auto-replace all files.
 *  - sourceHash != localBaseHash && localBaseHash != localCurrentHash:
 *    agent has modifications, snapshot for Learning Mode.
 *
 * force=true always overwrites (used by `reinstall-skills` command).
 */
export function installBuiltinSkills(opts = {}) {
    const targetDir = getSkillsDir();
    // Remove deprecated skills
    for (const name of DEPRECATED_SKILLS) {
        const skillDir = path.join(targetDir, name);
        if (fs.existsSync(skillDir)) {
            fs.rmSync(skillDir, { recursive: true, force: true });
            logger.system(`Removed deprecated skill: ${name}`);
        }
    }
    const sourceDir = getSkillsSourceDir();
    if (!sourceDir) {
        logger.debug("No built-in skills directory found");
        return;
    }
    logger.debug(`Using built-in skills source: ${sourceDir}`);
    if (!fs.existsSync(targetDir)) {
        fs.mkdirSync(targetDir, { recursive: true });
    }
    const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
    for (const entry of entries) {
        // Skip non-directories, the install.ts file itself, and deprecated skills
        if (!entry.isDirectory())
            continue;
        if (DEPRECATED_SKILLS.includes(entry.name))
            continue;
        const skillSource = path.join(sourceDir, entry.name);
        const skillTarget = path.join(targetDir, entry.name);
        const sourceSkillMd = path.join(skillSource, "SKILL.md");
        // Only process skills that have a SKILL.md in source
        if (!fs.existsSync(sourceSkillMd))
            continue;
        const targetSkillMd = path.join(skillTarget, "SKILL.md");
        if (!opts.force && fs.existsSync(targetSkillMd)) {
            const sourceHash = computeHash(fs.readFileSync(sourceSkillMd, "utf8"));
            const localBaseHash = readLocalBaseHash(skillTarget);
            const localCurrentHash = computeHash(fs.readFileSync(targetSkillMd, "utf8"));
            if (sourceHash === localBaseHash) {
                // Source already learned, nothing to do.
                logger.debug(`Skill "${entry.name}" is up to date`);
                continue;
            }
            if (sourceHash === localCurrentHash) {
                // Installed file already matches source (e.g. first run after
                // introducing .skill-local, or .skill-local was deleted).
                // Record the base hash so future runs use the normal three-hash path.
                writeLocalMeta(skillTarget, sourceHash);
                logger.debug(`Skill "${entry.name}" matches source, recorded base hash`);
                continue;
            }
            // Source has new content. Has the agent modified the local copy?
            if (localBaseHash === localCurrentHash) {
                // Agent has NOT modified the skill -- safe to auto-update all files.
                copySkillFiles(skillSource, skillTarget, true);
                writeLocalMeta(skillTarget, sourceHash);
                logger.system(`Auto-updated built-in skill: ${entry.name}`);
            }
            else {
                // Agent has modified the skill (or .skill-local missing) -- snapshot
                // all new skill files for Learning Mode review.
                queuePendingUpdate(entry.name, skillSource, sourceHash);
                logger.system(`Queued pending update for skill "${entry.name}" (agent has local modifications)`);
            }
            continue;
        }
        // Fresh install (or force mode)
        if (!fs.existsSync(skillTarget)) {
            fs.mkdirSync(skillTarget, { recursive: true });
        }
        copySkillFiles(skillSource, skillTarget, opts.force ?? false);
        // Record local base hash so future runs can detect agent modifications.
        const sourceHash = computeHash(fs.readFileSync(sourceSkillMd, "utf8"));
        writeLocalMeta(skillTarget, sourceHash);
        logger.system(`${opts.force ? "Reinstalled" : "Installed"} built-in skill: ${entry.name}`);
    }
}
/**
 * Recursively copy all files from skillSource into skillTarget.
 * When force=true, also removes files/dirs in the target that no longer
 * exist in the source (prevents stale artefacts after upstream renames
 * or deletions).
 */
function copySkillFiles(skillSource, skillTarget, force) {
    if (!fs.existsSync(skillTarget)) {
        fs.mkdirSync(skillTarget, { recursive: true });
    }
    const sourceEntries = fs.readdirSync(skillSource, { withFileTypes: true });
    const sourceNames = new Set(sourceEntries.map((e) => e.name));
    for (const entry of sourceEntries) {
        const src = path.join(skillSource, entry.name);
        const dst = path.join(skillTarget, entry.name);
        if (entry.isDirectory()) {
            // Recurse into subdirectories
            copySkillFiles(src, dst, force);
        }
        else {
            if (!force && fs.existsSync(dst))
                continue;
            fs.copyFileSync(src, dst);
        }
    }
    // When force-replacing, remove target entries that are no longer in source
    // (skip dotfiles like .skill-local which are our own metadata).
    if (force) {
        for (const entry of fs.readdirSync(skillTarget, { withFileTypes: true })) {
            if (entry.name.startsWith("."))
                continue;
            if (!sourceNames.has(entry.name)) {
                const stale = path.join(skillTarget, entry.name);
                fs.rmSync(stale, { recursive: true, force: true });
            }
        }
    }
}
/**
 * Write the local base hash for a skill.
 * Exported so the learning gate and skill tool can update it after changes.
 */
export function writeLocalMeta(skillTarget, hash) {
    fs.writeFileSync(path.join(skillTarget, ".skill-local"), hash, "utf8");
}
/**
 * Recursively snapshot all new skill files into pending-skill-updates/{skillName}/.
 * Also writes a `.update-meta` file with the source SKILL.md hash for the
 * learning gate to use after a successful merge.
 */
function queuePendingUpdate(skillName, skillSourceDir, sourceHash) {
    const dest = path.join(getPendingUpdatesDir(), skillName);
    copyDirRecursive(skillSourceDir, dest);
    // Store source hash for post-merge local metadata update
    fs.writeFileSync(path.join(dest, ".update-meta"), sourceHash, "utf8");
}
/** Recursively copy all files from src into dest, creating directories as needed. */
function copyDirRecursive(src, dest) {
    if (!fs.existsSync(dest)) {
        fs.mkdirSync(dest, { recursive: true });
    }
    for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
        const srcPath = path.join(src, entry.name);
        const destPath = path.join(dest, entry.name);
        if (entry.isDirectory()) {
            copyDirRecursive(srcPath, destPath);
        }
        else {
            fs.copyFileSync(srcPath, destPath);
        }
    }
}
//# sourceMappingURL=install.js.map