import { existsSync, readdirSync, statSync } from 'fs';
import { basename, join } from 'path';
import { getConfigPath } from '../config/paths.js';
import { parseCommandFile } from '../custom-commands/parser.js';
import { logError } from '../utils/message-queue.js';
const RESOURCES_DIR = 'resources';
const RELEVANCE_THRESHOLD = 5;
const MAX_COMMANDS_IN_CONTEXT = 3;
/**
 * Validate that a directory entry doesn't contain path traversal patterns.
 */
function isSafeEntry(entry) {
    return (entry !== '..' &&
        entry !== '.' &&
        !entry.includes('/') &&
        !entry.includes('\\'));
}
export class CustomCommandLoader {
    commands = new Map();
    aliases = new Map(); // alias -> command name
    projectCommandsDir;
    personalCommandsDir;
    constructor(projectRoot = process.cwd()) {
        // nosemgrep
        this.projectCommandsDir = join(projectRoot, '.nanocoder', 'commands'); // nosemgrep
        this.personalCommandsDir = join(getConfigPath(), 'commands');
    }
    /**
     * Load all custom commands from both project and personal directories
     */
    loadCommands() {
        this.commands.clear();
        this.aliases.clear();
        // Load personal commands first (lower priority)
        if (existsSync(this.personalCommandsDir)) {
            this.scanDirectory(this.personalCommandsDir, undefined, 'personal');
        }
        // Load project commands (higher priority, overrides personal)
        if (existsSync(this.projectCommandsDir)) {
            this.scanDirectory(this.projectCommandsDir, undefined, 'project');
        }
    }
    /**
     * Recursively scan directory for .md files, supporting directory-as-command
     */
    scanDirectory(dir, namespace, source) {
        const entries = readdirSync(dir);
        for (const entry of entries) {
            if (!isSafeEntry(entry))
                continue;
            const fullPath = join(dir, entry); // nosemgrep
            const stat = statSync(fullPath);
            if (stat.isDirectory()) {
                // Check if this is a directory-as-command pattern:
                // directory contains <dirname>.md + optional resources/
                const commandFile = join(fullPath, `${entry}.md`); // nosemgrep
                if (existsSync(commandFile)) {
                    this.loadCommand(commandFile, namespace, source, fullPath);
                }
                else {
                    // Regular subdirectory becomes a namespace
                    const subNamespace = namespace ? `${namespace}:${entry}` : entry;
                    this.scanDirectory(fullPath, subNamespace, source);
                }
            }
            else if (entry.endsWith('.md')) {
                // Parse and register command
                this.loadCommand(fullPath, namespace, source);
            }
        }
    }
    /**
     * Load a single command file.
     * If commandDir is provided, also load resources from its resources/ subdirectory.
     */
    loadCommand(filePath, namespace, source, commandDir) {
        try {
            const parsed = parseCommandFile(filePath);
            const commandName = basename(filePath, '.md');
            const fullName = namespace ? `${namespace}:${commandName}` : commandName;
            // Load resources if this is a directory-based command
            let loadedResources;
            if (commandDir) {
                loadedResources = this.loadResources(commandDir);
            }
            // Get file modification time
            let lastModified;
            try {
                const st = statSync(filePath);
                lastModified = st.mtime;
            }
            catch {
                // ignore
            }
            const command = {
                name: commandName,
                path: filePath,
                namespace,
                fullName,
                metadata: parsed.metadata,
                content: parsed.content,
                source,
                lastModified,
                loadedResources: loadedResources && loadedResources.length > 0
                    ? loadedResources
                    : undefined,
                subscribe: parsed.subscribe,
            };
            // Register main command (project commands override personal with same name)
            this.commands.set(fullName, command);
            // Register aliases
            if (parsed.metadata.aliases) {
                for (const alias of parsed.metadata.aliases) {
                    const fullAlias = namespace ? `${namespace}:${alias}` : alias;
                    this.aliases.set(fullAlias, fullName);
                }
            }
        }
        catch (error) {
            logError(`Failed to load custom command from ${filePath}: ${String(error)}`);
        }
    }
    /**
     * Load resources from a command's resources/ subdirectory
     */
    loadResources(commandDir) {
        const resourcesDir = join(commandDir, RESOURCES_DIR); // nosemgrep
        if (!existsSync(resourcesDir)) {
            return [];
        }
        const entries = readdirSync(resourcesDir);
        const resources = [];
        for (const entry of entries) {
            if (!isSafeEntry(entry))
                continue;
            const resourcePath = join(resourcesDir, entry); // nosemgrep
            let st;
            try {
                st = statSync(resourcePath);
            }
            catch {
                continue;
            }
            if (!st.isFile())
                continue;
            const ext = entry.toLowerCase().slice(entry.lastIndexOf('.'));
            let type = 'document';
            if (['.py', '.js', '.sh', '.bat', '.ts'].includes(ext)) {
                type = 'script';
            }
            else if (['.txt', '.md'].includes(ext)) {
                type = entry.endsWith('.template') ? 'template' : 'document';
            }
            else if (['.json', '.yaml', '.yml', '.toml'].includes(ext)) {
                type = 'config';
            }
            const executable = Boolean(type === 'script' && st.mode & 0o111);
            resources.push({
                name: entry,
                path: resourcePath,
                type,
                executable: executable || undefined,
            });
        }
        return resources;
    }
    /**
     * Register a command produced outside this loader (e.g. by the skill
     * registrar). Returns true on success. Returns false if a command with
     * the same name is already registered - the caller is expected to treat
     * the collision as a hard error.
     */
    registerExternal(command) {
        // Key by fullName to stay consistent with the legacy loadCommand path,
        // which indexes the same Map by `fullName` (so namespaced bundle
        // commands like `k8s:status` are findable via /k8s:status).
        const key = command.fullName;
        if (this.commands.has(key))
            return false;
        this.commands.set(key, command);
        const aliasPrefix = command.namespace ? `${command.namespace}:` : '';
        for (const alias of command.metadata.aliases ?? []) {
            const fullAlias = `${aliasPrefix}${alias}`;
            if (!this.aliases.has(fullAlias))
                this.aliases.set(fullAlias, key);
        }
        return true;
    }
    /**
     * Remove a previously-registered command by fullName. Returns true if
     * found.
     */
    unregisterExternal(fullName) {
        const cmd = this.commands.get(fullName);
        if (!cmd)
            return false;
        this.commands.delete(fullName);
        for (const [alias, target] of this.aliases) {
            if (target === fullName)
                this.aliases.delete(alias);
        }
        return true;
    }
    /**
     * Get a command by name (checking aliases too)
     */
    getCommand(name) {
        // Check direct command name
        const command = this.commands.get(name);
        if (command)
            return command;
        // Check aliases
        const aliasTarget = this.aliases.get(name);
        if (aliasTarget) {
            return this.commands.get(aliasTarget);
        }
        return undefined;
    }
    /**
     * Get all available commands
     */
    getAllCommands() {
        return Array.from(this.commands.values());
    }
    /**
     * Get only commands that participate in auto-injection
     * (those with triggers or tags defined)
     */
    getAutoInjectableCommands() {
        return this.getAllCommands().filter(cmd => cmd.metadata.triggers?.length || cmd.metadata.tags?.length);
    }
    /**
     * Get command suggestions for autocomplete
     */
    getSuggestions(prefix) {
        const suggestions = [];
        const lowerPrefix = prefix.toLowerCase();
        // Add matching command names
        for (const [name, _command] of this.commands.entries()) {
            if (name.toLowerCase().startsWith(lowerPrefix)) {
                suggestions.push(name);
            }
        }
        // Add matching aliases
        for (const [alias, _target] of this.aliases.entries()) {
            if (alias.toLowerCase().startsWith(lowerPrefix) &&
                !suggestions.includes(alias)) {
                suggestions.push(alias);
            }
        }
        return suggestions.sort();
    }
    /**
     * Find relevant commands for auto-injection based on user request
     */
    findRelevantCommands(request, availableTools) {
        const requestLower = request.toLowerCase();
        const scored = [];
        for (const command of this.getAutoInjectableCommands()) {
            const score = this.calculateRelevanceScore(command, requestLower, availableTools);
            if (score >= RELEVANCE_THRESHOLD) {
                scored.push({ command, score });
            }
        }
        scored.sort((a, b) => b.score - a.score);
        return scored.slice(0, MAX_COMMANDS_IN_CONTEXT).map(s => s.command);
    }
    /**
     * Calculate relevance score for a command against a user request
     */
    calculateRelevanceScore(command, requestLower, availableTools) {
        let score = 0;
        const meta = command.metadata;
        if (meta.description?.toLowerCase().includes(requestLower)) {
            score += 10;
        }
        if (meta.category?.toLowerCase().includes(requestLower)) {
            score += 5;
        }
        if (meta.triggers?.length) {
            for (const trigger of meta.triggers) {
                if (requestLower.includes(trigger.toLowerCase())) {
                    score += 15;
                }
            }
        }
        if (meta.tags?.length) {
            for (const tag of meta.tags) {
                if (requestLower.includes(tag.toLowerCase())) {
                    score += 5;
                }
            }
        }
        return score;
    }
    /**
     * Check if commands directory exists
     */
    hasCustomCommands() {
        return (existsSync(this.projectCommandsDir) ||
            existsSync(this.personalCommandsDir));
    }
    /**
     * Get the project commands directory path
     */
    getCommandsDirectory() {
        return this.projectCommandsDir;
    }
    /**
     * Get the personal commands directory path
     */
    getPersonalCommandsDirectory() {
        return this.personalCommandsDir;
    }
}
//# sourceMappingURL=loader.js.map