/**
 * AgentController definition discovery, validation, and loading.
 *
 * User-defined subagents are markdown files with YAML frontmatter containing
 * agent configuration (name, description, tools, model, thinkingLevel, etc.).
 * The markdown body becomes the system prompt.
 *
 * Discovered from user dir (~/.shortcut/agent/agents/), project dir
 * (<cwd>/.shortcut/agents/), and explicit paths. Validates names (kebab-case),
 * descriptions, tool arrays, and thinking levels per the agent definition spec.
 */
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs';
import ignore from 'ignore';
import { homedir } from 'os';
import { isAbsolute, join, relative, resolve, sep } from 'path';
import { CONFIG_DIR_NAME, getAgentDir } from '../../config.js';
import { isValidThinkingLevel, VALID_THINKING_LEVELS } from '../../core/defaults.js';
import { parseFrontmatter } from '../../shared/content/frontmatter.js';
// =============================================================================
// Constants
// =============================================================================
/** Max name length per spec */
const MAX_NAME_LENGTH = 64;
/** Max description length per spec */
const MAX_DESCRIPTION_LENGTH = 1024;
/** Known frontmatter fields — anything else triggers an unknown-field warning. */
const KNOWN_FRONTMATTER_FIELDS = new Set([
    'name',
    'description',
    'tools',
    'model',
    'thinkingLevel',
    'timeoutSeconds'
]);
/** Built-in agent names that user agents must not collide with. */
export const BUILT_IN_AGENT_NAMES = [
    'clone',
    'document_reader',
    'general',
    'verification'
];
const BUILT_IN_AGENT_SET = new Set(BUILT_IN_AGENT_NAMES);
const IGNORE_FILE_NAMES = ['.gitignore', '.ignore', '.fdignore'];
export const AGENT_SOURCE_PRECEDENCE = ['user', 'project', 'explicit'];
const AGENT_SOURCE_LABEL = {
    user: 'user',
    project: 'project',
    explicit: 'explicit'
};
function toPosixPath(p) {
    return p.split(sep).join('/');
}
function prefixIgnorePattern(line, prefix) {
    const trimmed = line.trim();
    if (!trimmed)
        return null;
    if (trimmed.startsWith('#') && !trimmed.startsWith('\\#'))
        return null;
    let pattern = line;
    let negated = false;
    if (pattern.startsWith('!')) {
        negated = true;
        pattern = pattern.slice(1);
    }
    else if (pattern.startsWith('\\!')) {
        pattern = pattern.slice(1);
    }
    if (pattern.startsWith('/')) {
        pattern = pattern.slice(1);
    }
    const prefixed = prefix ? `${prefix}${pattern}` : pattern;
    return negated ? `!${prefixed}` : prefixed;
}
function addIgnoreRules(ig, dir, rootDir) {
    const relativeDir = relative(rootDir, dir);
    const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : '';
    for (const filename of IGNORE_FILE_NAMES) {
        const ignorePath = join(dir, filename);
        if (!existsSync(ignorePath))
            continue;
        try {
            const content = readFileSync(ignorePath, 'utf-8');
            const patterns = content
                .split(/\r?\n/)
                .map((line) => prefixIgnorePattern(line, prefix))
                .filter((line) => Boolean(line));
            if (patterns.length > 0) {
                ig.add(patterns);
            }
        }
        catch { }
    }
}
// =============================================================================
// Validation
// =============================================================================
function validateAgentName(name) {
    const errors = [];
    if (!name) {
        errors.push('name is required');
        return errors;
    }
    if (name.length > MAX_NAME_LENGTH) {
        errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
    }
    if (!/^[a-z0-9-]+$/.test(name)) {
        errors.push('name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)');
    }
    if (name.startsWith('-') || name.endsWith('-')) {
        errors.push('name must not start or end with a hyphen');
    }
    if (name.includes('--')) {
        errors.push('name must not contain consecutive hyphens');
    }
    if (BUILT_IN_AGENT_SET.has(name)) {
        errors.push(`name "${name}" conflicts with built-in agent (built-ins: ${BUILT_IN_AGENT_NAMES.join(', ')})`);
    }
    return errors;
}
function validateDescription(description) {
    if (!description || description.trim() === '') {
        return ['description is required'];
    }
    if (description.length > MAX_DESCRIPTION_LENGTH) {
        return [`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`];
    }
    return [];
}
function validateTools(tools) {
    if (!Array.isArray(tools)) {
        return ['tools must be an array of strings'];
    }
    if (tools.length === 0) {
        return ['tools must contain at least one tool name'];
    }
    for (let i = 0; i < tools.length; i++) {
        if (typeof tools[i] !== 'string' || tools[i].trim() === '') {
            return [`tools[${i}] must be a non-empty string`];
        }
    }
    return [];
}
// =============================================================================
// Single-file loader
// =============================================================================
function loadAgentFromFile(filePath, source) {
    const diagnostics = [];
    try {
        const rawContent = readFileSync(filePath, 'utf-8');
        const { frontmatter, body } = parseFrontmatter(rawContent);
        // --- Warn on unknown frontmatter fields (forward compatibility) ---
        for (const key of Object.keys(frontmatter)) {
            if (!KNOWN_FRONTMATTER_FIELDS.has(key)) {
                diagnostics.push({
                    type: 'warning',
                    message: `unknown frontmatter field "${key}" — will be ignored`,
                    path: filePath
                });
            }
        }
        // --- Name (required, from frontmatter only) ---
        const name = frontmatter.name;
        if (!name) {
            diagnostics.push({
                type: 'warning',
                message: 'name is required in frontmatter',
                path: filePath
            });
            return { agent: null, diagnostics };
        }
        const nameErrors = validateAgentName(name);
        for (const error of nameErrors) {
            diagnostics.push({ type: 'warning', message: error, path: filePath });
        }
        if (nameErrors.length > 0) {
            return { agent: null, diagnostics };
        }
        // --- Description (required) ---
        const descErrors = validateDescription(frontmatter.description);
        for (const error of descErrors) {
            diagnostics.push({ type: 'warning', message: error, path: filePath });
        }
        if (descErrors.length > 0) {
            return { agent: null, diagnostics };
        }
        // --- Tools (required, non-empty string array) ---
        const toolErrors = validateTools(frontmatter.tools);
        for (const error of toolErrors) {
            diagnostics.push({ type: 'warning', message: error, path: filePath });
        }
        if (toolErrors.length > 0) {
            return { agent: null, diagnostics };
        }
        const tools = frontmatter.tools;
        // Security: warn about recursive subagent spawning risk
        if (tools.includes('task')) {
            diagnostics.push({
                type: 'warning',
                message: 'agent includes "task" tool — this allows recursive subagent spawning, which may cause unbounded cost',
                path: filePath
            });
        }
        // --- System prompt (body after frontmatter, required) ---
        const systemPrompt = body.trim();
        if (!systemPrompt) {
            diagnostics.push({
                type: 'warning',
                message: 'agent body (system prompt) is required — the markdown content after frontmatter must not be empty',
                path: filePath
            });
            return { agent: null, diagnostics };
        }
        // --- Thinking level (optional, validated) ---
        let thinkingLevel;
        if (frontmatter.thinkingLevel !== undefined) {
            if (isValidThinkingLevel(frontmatter.thinkingLevel)) {
                thinkingLevel = frontmatter.thinkingLevel;
            }
            else {
                diagnostics.push({
                    type: 'warning',
                    message: `invalid thinkingLevel "${frontmatter.thinkingLevel}" — must be one of: ${VALID_THINKING_LEVELS.join(', ')}`,
                    path: filePath
                });
                // Non-fatal: proceed without thinkingLevel
            }
        }
        // --- Model (optional) ---
        const model = typeof frontmatter.model === 'string' ? frontmatter.model : undefined;
        // --- Timeout (optional, must be positive number) ---
        const timeoutSeconds = typeof frontmatter.timeoutSeconds === 'number' && frontmatter.timeoutSeconds > 0
            ? frontmatter.timeoutSeconds
            : undefined;
        return {
            agent: {
                definition: {
                    name,
                    description: frontmatter.description,
                    systemPrompt,
                    tools,
                    model,
                    thinkingLevel,
                    timeoutSeconds
                },
                filePath,
                source
            },
            diagnostics
        };
    }
    catch (error) {
        const message = error instanceof Error ? error.message : 'failed to parse agent definition file';
        diagnostics.push({ type: 'warning', message, path: filePath });
        return { agent: null, diagnostics };
    }
}
/**
 * Load agent definitions from a directory.
 *
 * Scans for .md files in the given directory (flat — no subdirectory recursion).
 * Respects .gitignore, .ignore, and .fdignore files. Skips dotfiles and
 * node_modules. Follows symlinks.
 */
function loadAgentsFromDirInternal(dir, source = 'explicit') {
    const agents = [];
    const diagnostics = [];
    if (!existsSync(dir)) {
        return { agents, diagnostics };
    }
    const ig = ignore();
    addIgnoreRules(ig, dir, dir);
    try {
        const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
        for (const entry of entries) {
            if (entry.name.startsWith('.'))
                continue;
            if (entry.name === 'node_modules')
                continue;
            const fullPath = join(dir, entry.name);
            // Resolve symlinks
            let isFile = entry.isFile();
            if (entry.isSymbolicLink()) {
                try {
                    const stats = statSync(fullPath);
                    isFile = stats.isFile();
                }
                catch {
                    // Broken symlink — skip
                    continue;
                }
            }
            if (!isFile || !entry.name.endsWith('.md'))
                continue;
            const relPath = toPosixPath(relative(dir, fullPath));
            if (ig.ignores(relPath))
                continue;
            const result = loadAgentFromFile(fullPath, source);
            if (result.agent) {
                agents.push(result.agent);
            }
            diagnostics.push(...result.diagnostics);
        }
    }
    catch { }
    return { agents, diagnostics };
}
/** Public API — returns AgentDefinition[] without internal filePath tracking. */
export function loadAgentsFromDir(options) {
    const { dir } = options;
    const result = loadAgentsFromDirInternal(dir);
    return {
        agents: result.agents.map((a) => a.definition),
        diagnostics: result.diagnostics
    };
}
// =============================================================================
// Top-level loader
// =============================================================================
function normalizePath(input) {
    const trimmed = input.trim();
    if (trimmed === '~')
        return homedir();
    if (trimmed.startsWith('~/'))
        return join(homedir(), trimmed.slice(2));
    if (trimmed.startsWith('~'))
        return join(homedir(), trimmed.slice(1));
    return trimmed;
}
function resolveAgentPath(p, cwd) {
    const normalized = normalizePath(p);
    return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
}
/**
 * Load agent definitions from all configured locations.
 *
 * Discovery order (first-loaded wins on name collision):
 *   1. User agents:    <agentDir>/agents/
 *   2. Project agents: <cwd>/.shortcut/agents/
 *   3. Explicit agentPaths (files or directories)
 */
export function loadAgents(options = {}) {
    const { cwd = process.cwd(), agentDir, agentPaths = [], includeDefaults = true } = options;
    const resolvedAgentDir = agentDir ?? getAgentDir();
    const agentMap = new Map();
    const realPathSet = new Set();
    const allDiagnostics = [];
    const collisionDiagnostics = [];
    function addAgents(result) {
        allDiagnostics.push(...result.diagnostics);
        for (const agent of result.agents) {
            // Resolve symlinks to detect duplicate files
            let realPath;
            try {
                realPath = realpathSync(agent.filePath);
            }
            catch {
                realPath = agent.filePath;
            }
            // Skip silently if we've already loaded this exact file (via symlink)
            if (realPathSet.has(realPath))
                continue;
            const existing = agentMap.get(agent.definition.name);
            if (existing) {
                collisionDiagnostics.push({
                    type: 'collision',
                    message: `name "${agent.definition.name}" collision (${AGENT_SOURCE_LABEL[existing.source]} definition wins over ${AGENT_SOURCE_LABEL[agent.source]} definition by discovery precedence)`,
                    path: agent.filePath,
                    collision: {
                        resourceType: 'agent',
                        name: agent.definition.name,
                        winnerPath: existing.filePath,
                        loserPath: agent.filePath,
                        winnerSource: AGENT_SOURCE_LABEL[existing.source],
                        loserSource: AGENT_SOURCE_LABEL[agent.source]
                    }
                });
            }
            else {
                agentMap.set(agent.definition.name, agent);
                realPathSet.add(realPath);
            }
        }
    }
    if (includeDefaults) {
        const userAgentsDir = join(resolvedAgentDir, 'agents');
        const projectAgentsDir = resolve(cwd, CONFIG_DIR_NAME, 'agents');
        addAgents(loadAgentsFromDirInternal(userAgentsDir, 'user'));
        addAgents(loadAgentsFromDirInternal(projectAgentsDir, 'project'));
    }
    for (const rawPath of agentPaths) {
        const resolvedPath = resolveAgentPath(rawPath, cwd);
        if (!existsSync(resolvedPath)) {
            allDiagnostics.push({
                type: 'warning',
                message: 'agent path does not exist',
                path: resolvedPath
            });
            continue;
        }
        try {
            const stats = statSync(resolvedPath);
            if (stats.isDirectory()) {
                addAgents(loadAgentsFromDirInternal(resolvedPath, 'explicit'));
            }
            else if (stats.isFile() && resolvedPath.endsWith('.md')) {
                const result = loadAgentFromFile(resolvedPath, 'explicit');
                if (result.agent) {
                    addAgents({ agents: [result.agent], diagnostics: result.diagnostics });
                }
                else {
                    allDiagnostics.push(...result.diagnostics);
                }
            }
            else {
                allDiagnostics.push({
                    type: 'warning',
                    message: 'agent path is not a markdown file',
                    path: resolvedPath
                });
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : 'failed to read agent path';
            allDiagnostics.push({ type: 'warning', message, path: resolvedPath });
        }
    }
    return {
        agents: Array.from(agentMap.values()).map((a) => a.definition),
        diagnostics: [...allDiagnostics, ...collisionDiagnostics]
    };
}
//# sourceMappingURL=agents.js.map