/**
 * Skill File Parser
 *
 * Parses agent instruction files from 8 different AI frameworks into a common
 * format for scanning. Supports SKILL.md, HOOK.md, handler.js, .cursorrules,
 * .windsurfrules, .clinerules, CLAUDE.md, copilot-instructions.md,
 * .aider.conf.yml, and .continue/config.json.
 *
 * The parser never throws — malformed input produces sensible defaults.
 */
import * as fs from 'node:fs';
import * as path from 'node:path';
// ── Format Detection ──
/**
 * Auto-detect the skill format based on the file path.
 * Uses basename for filename matching; case-insensitive for .md files.
 */
export function detectFormat(filePath) {
    const basename = path.basename(filePath);
    const lower = basename.toLowerCase();
    // Exact filename matches (case-insensitive for .md)
    if (lower === 'skill.md')
        return 'skill-md';
    if (lower === 'hook.md')
        return 'hook-md';
    if (basename === 'handler.js')
        return 'hook-js';
    if (lower === 'claude.md')
        return 'claude-md';
    if (lower === 'copilot-instructions.md')
        return 'copilot-md';
    if (basename === '.aider.conf.yml')
        return 'aider-yml';
    // Rules files (exact basename match, case-sensitive — these are dotfiles)
    if (basename === '.cursorrules' || basename === '.windsurfrules' || basename === '.clinerules') {
        return 'rules';
    }
    // Path-based matches
    const normalised = filePath.replace(/\\/g, '/');
    if (normalised.includes('.claude/commands/'))
        return 'claude-md';
    if (basename === 'config.json' && normalised.includes('.continue/')) {
        return 'continue-json';
    }
    return 'unknown';
}
// ── Simple YAML Parser ──
/**
 * Parse simple YAML key-value pairs, supporting one level of nesting.
 *
 * This does NOT handle the full YAML specification — only the straightforward
 * frontmatter structures used by skill and hook files:
 *   key: value
 *   key: "quoted value"
 *   parent:
 *     child: value
 */
function parseSimpleYaml(yamlText) {
    const result = {};
    const lines = yamlText.split('\n');
    let currentParent = null;
    let nestedObj = null;
    for (const line of lines) {
        // Skip empty lines and comments
        const trimmed = line.trimEnd();
        if (trimmed === '' || trimmed.startsWith('#')) {
            continue;
        }
        // Determine indentation level
        const indent = line.length - line.trimStart().length;
        const content = trimmed.trim();
        // Match key: value pattern
        const kvMatch = content.match(/^([A-Za-z_][\w.-]*)\s*:\s*(.*)/);
        if (!kvMatch)
            continue;
        const key = kvMatch[1];
        let value = kvMatch[2].trim();
        if (indent >= 2 && currentParent && nestedObj) {
            // This is a nested key under the current parent
            nestedObj[key] = unquoteValue(value);
        }
        else {
            // Top-level key — flush any previous nested object
            if (currentParent && nestedObj) {
                result[currentParent] = nestedObj;
                currentParent = null;
                nestedObj = null;
            }
            if (value === '' || value === '') {
                // This key has no inline value — it introduces a nested block
                currentParent = key;
                nestedObj = {};
            }
            else {
                result[key] = unquoteValue(value);
            }
        }
    }
    // Flush trailing nested object
    if (currentParent && nestedObj) {
        result[currentParent] = nestedObj;
    }
    return result;
}
/**
 * Remove surrounding quotes from a YAML value string and coerce simple types.
 */
function unquoteValue(raw) {
    if (raw === '')
        return '';
    // Remove surrounding quotes (single or double)
    if ((raw.startsWith('"') && raw.endsWith('"')) ||
        (raw.startsWith("'") && raw.endsWith("'"))) {
        return raw.slice(1, -1);
    }
    // Boolean coercion
    if (raw === 'true')
        return true;
    if (raw === 'false')
        return false;
    // Numeric coercion
    if (/^-?\d+(\.\d+)?$/.test(raw)) {
        return Number(raw);
    }
    return raw;
}
// ── Frontmatter Extraction ──
/**
 * Split YAML frontmatter (between --- markers at the start of a file)
 * from the markdown body. Returns [frontmatter, body].
 */
function splitFrontmatter(content) {
    // Frontmatter must start at the very beginning of the file
    if (!content.startsWith('---')) {
        return [null, content];
    }
    // Find the closing --- marker (must be on its own line)
    const closingIndex = content.indexOf('\n---', 3);
    if (closingIndex === -1) {
        // No closing marker — treat the entire file as body
        return [null, content];
    }
    const frontmatter = content.slice(3, closingIndex).trim();
    const body = content.slice(closingIndex + 4).trim();
    return [frontmatter, body];
}
// ── Name Extraction Helpers ──
/**
 * Try to extract a name from the first line comment in a JS file,
 * For example: "// my-hook" or block comments like "Hook Name".
 */
function extractJsName(source) {
    const firstLine = source.split('\n')[0]?.trim() ?? '';
    // Single-line comment: // name
    const singleComment = firstLine.match(/^\/\/\s*(.+)/);
    if (singleComment) {
        return singleComment[1].trim();
    }
    // Block comment: /* name */
    const blockComment = firstLine.match(/^\/\*\s*(.+?)\s*\*\//);
    if (blockComment) {
        return blockComment[1].trim();
    }
    // module.exports name — look for module.exports.name = "..."
    const exportsName = source.match(/module\.exports\.name\s*=\s*["']([^"']+)["']/);
    if (exportsName) {
        return exportsName[1];
    }
    // module.exports = { name: "..." }
    const objName = source.match(/module\.exports\s*=\s*\{[^}]*name\s*:\s*["']([^"']+)["']/);
    if (objName) {
        return objName[1];
    }
    return null;
}
/**
 * Derive a name from a filename, stripping the extension and leading dot.
 */
function nameFromFilename(filename) {
    const basename = path.basename(filename);
    const ext = path.extname(basename);
    let name = ext ? basename.slice(0, -ext.length) : basename;
    // Strip leading dot for dotfiles (e.g. .cursorrules -> cursorrules)
    if (name.startsWith('.')) {
        name = name.slice(1);
    }
    return name || 'unknown';
}
// ── Content-Based Format Detection ──
/**
 * Auto-detect the skill format based on the content itself (no file path).
 * Used when pasting content in the cloud dashboard with "Auto-detect" selected.
 */
export function detectFormatFromContent(content) {
    const trimmed = content.trim();
    // JSON-like content → continue-json
    if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
        try {
            const parsed = JSON.parse(trimmed);
            if (parsed.models || parsed.tabAutocompleteModel || parsed.customCommands) {
                return 'continue-json';
            }
        }
        catch { /* not valid JSON */ }
    }
    // YAML frontmatter with name/description → skill-md or hook-md
    if (trimmed.startsWith('---')) {
        const closingIdx = trimmed.indexOf('\n---', 3);
        if (closingIdx !== -1) {
            const frontmatter = trimmed.slice(3, closingIdx).toLowerCase();
            if (frontmatter.includes('trigger:') || frontmatter.includes('hook')) {
                return 'hook-md';
            }
            return 'skill-md';
        }
    }
    // JavaScript patterns → hook-js
    if (/^(\/\/|\/\*|import\s|export\s|const\s|module\.exports)/.test(trimmed) &&
        (trimmed.includes('module.exports') || trimmed.includes('export default') || trimmed.includes('export const'))) {
        return 'hook-js';
    }
    // YAML patterns (key: value) with aider-specific keys → aider-yml
    if (/^(model|edit-format|auto-commits|lint-cmd)\s*:/m.test(trimmed)) {
        return 'aider-yml';
    }
    // Markdown with cursor/windsurf/cline rule patterns
    const lowerContent = trimmed.toLowerCase();
    if (lowerContent.includes('you are a') ||
        lowerContent.includes('rules:') ||
        lowerContent.includes('always follow') ||
        lowerContent.includes('coding style')) {
        // Check for copilot-instructions patterns
        if (lowerContent.includes('github copilot') || lowerContent.includes('copilot')) {
            return 'copilot-md';
        }
        return 'rules';
    }
    // Markdown starting with # heading → could be claude-md or skill-md
    if (trimmed.startsWith('#')) {
        if (lowerContent.includes('claude') && lowerContent.includes('project')) {
            return 'claude-md';
        }
        return 'skill-md';
    }
    return 'unknown';
}
// ── Core Parser ──
/**
 * Parse file content into a ParsedSkill structure.
 *
 * @param content  Raw file content
 * @param format   Detected format (defaults to auto-detected from content)
 */
export function parseSkillFile(content, format) {
    const effectiveFormat = format ?? detectFormatFromContent(content);
    const raw = content;
    // Guard against empty or binary-looking content
    if (!content || content.length === 0) {
        return {
            name: 'unknown',
            format: effectiveFormat,
            content: '',
            raw,
        };
    }
    // Detect likely binary content (high ratio of non-printable characters)
    const nonPrintable = content.slice(0, 1024).replace(/[\x20-\x7E\t\n\r]/g, '').length;
    if (nonPrintable > 128) {
        return {
            name: 'unknown',
            format: effectiveFormat,
            content: '',
            raw,
        };
    }
    switch (effectiveFormat) {
        case 'skill-md':
        case 'hook-md':
            return parseMarkdownWithFrontmatter(content, effectiveFormat, raw);
        case 'rules':
        case 'claude-md':
        case 'copilot-md':
            return parseRawContent(content, effectiveFormat, raw);
        case 'hook-js':
            return parseJavaScript(content, raw);
        case 'aider-yml':
            return parseAiderYaml(content, raw);
        case 'continue-json':
            return parseContinueJson(content, raw);
        case 'unknown':
        default:
            return {
                name: 'unknown',
                format: 'unknown',
                content,
                raw,
            };
    }
}
/**
 * Parse a markdown file that may contain YAML frontmatter (skill-md, hook-md).
 */
function parseMarkdownWithFrontmatter(content, format, raw) {
    const [frontmatter, body] = splitFrontmatter(content);
    let metadata;
    let name = 'unknown';
    if (frontmatter) {
        try {
            metadata = parseSimpleYaml(frontmatter);
        }
        catch {
            // Malformed YAML — continue without metadata
            metadata = undefined;
        }
        if (metadata && typeof metadata.name === 'string' && metadata.name.length > 0) {
            name = metadata.name;
        }
    }
    return {
        name,
        format,
        content: body,
        metadata,
        raw,
    };
}
/**
 * Parse a raw content file with no frontmatter (rules, claude-md, copilot-md).
 * The name is derived from the filename if available, otherwise defaults.
 */
function parseRawContent(content, format, raw) {
    // Name will be overridden by readSkillFile when filePath is available
    return {
        name: 'unknown',
        format,
        content,
        raw,
    };
}
/**
 * Parse a JavaScript hook file.
 */
function parseJavaScript(content, raw) {
    const extracted = extractJsName(content);
    return {
        name: extracted ?? 'handler',
        format: 'hook-js',
        content,
        raw,
    };
}
/**
 * Parse an Aider YAML configuration file.
 */
function parseAiderYaml(content, raw) {
    let metadata;
    try {
        metadata = parseSimpleYaml(content);
    }
    catch {
        // Malformed YAML — continue without metadata
        metadata = undefined;
    }
    return {
        name: 'aider',
        format: 'aider-yml',
        content,
        metadata,
        raw,
    };
}
/**
 * Parse a Continue JSON configuration file.
 */
function parseContinueJson(content, raw) {
    let metadata;
    let parsedContent = content;
    let name = 'continue';
    try {
        const parsed = JSON.parse(content);
        metadata = parsed;
        parsedContent = JSON.stringify(parsed);
        if (typeof parsed.name === 'string' && parsed.name.length > 0) {
            name = parsed.name;
        }
    }
    catch {
        // Malformed JSON — use raw content as-is
        metadata = undefined;
        parsedContent = content;
    }
    return {
        name,
        format: 'continue-json',
        content: parsedContent,
        metadata,
        raw,
    };
}
// ── File Reader ──
/**
 * Read a skill file from disc, auto-detect its format, and parse it.
 *
 * Returns a ParsedSkill with the filePath field populated.
 * If the file cannot be read, returns a minimal ParsedSkill with empty content.
 */
export function readSkillFile(filePath) {
    let content;
    try {
        content = fs.readFileSync(filePath, 'utf-8');
    }
    catch {
        // File unreadable — return a safe default
        return {
            name: nameFromFilename(filePath),
            format: detectFormat(filePath),
            content: '',
            raw: '',
            filePath,
        };
    }
    const format = detectFormat(filePath);
    const result = parseSkillFile(content, format);
    result.filePath = filePath;
    // For formats that derive their name from the filename, apply it now
    if (result.name === 'unknown' || result.name === '') {
        result.name = nameFromFilename(filePath);
    }
    return result;
}
