/**
 * Vigthoria CLI — Semantic Context Ranker
 *
 * Lightweight local-first file relevance scoring. Extracts keywords from the
 * user prompt and scores workspace files by occurrence in filename + content.
 * Used to prioritise which files are sent to V3 as workspace context.
 *
 * No external dependencies — uses only Node.js built-ins.
 */
import fs from 'node:fs';
import path from 'node:path';
const IGNORED_DIRS = new Set([
    '.git', 'node_modules', 'dist', '.next', '__pycache__',
    '.vigthoria', 'coverage', '.cache', '.turbo', 'build', 'out',
]);
const TEXT_EXTENSIONS = new Set([
    '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
    '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.cs', '.php', '.rb',
    '.json', '.yaml', '.yml', '.toml', '.env', '.sh', '.bash',
    '.html', '.css', '.scss', '.sass', '.less',
    '.md', '.txt', '.xml', '.svg',
]);
/** Extract scoring keywords from the user prompt. */
function extractKeywords(prompt) {
    // General words (min length 4)
    const words = prompt
        .replace(/[^\w\s./\\-]/g, ' ')
        .split(/\s+/)
        .filter((w) => w.length >= 4)
        .map((w) => w.toLowerCase());
    // CamelCase splits
    const camel = (prompt.match(/[a-z][A-Z][a-zA-Z]+|[A-Z]{2,}[a-z]+[a-zA-Z]*/g) || [])
        .map((w) => w.toLowerCase());
    // Acronyms: uppercase sequences 2-4 chars (SSE, API, HTTP, etc.)
    const acronyms = (prompt.match(/\b[A-Z]{2,4}\b/g) || [])
        .map((w) => w.toLowerCase());
    return [...new Set([...words, ...camel, ...acronyms])].filter((kw) => kw.length >= 2);
}
function scoreFile(filePath, keywords) {
    if (keywords.length === 0)
        return 0;
    const filename = path.basename(filePath).toLowerCase();
    const parentDir = path.basename(path.dirname(filePath)).toLowerCase();
    let score = 0;
    for (const kw of keywords) {
        // Filename match (high weight)
        if (filename.includes(kw))
            score += 15;
        // Parent directory name match (medium weight)
        if (parentDir.includes(kw))
            score += 6;
    }
    try {
        const content = fs.readFileSync(filePath, 'utf-8').slice(0, 300000);
        const lower = content.toLowerCase();
        for (const kw of keywords) {
            const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            // Word boundaries for short keywords prevent false-positives (e.g. 'sse' inside 'assert')
            const pattern = kw.length <= 4 ? '\\b' + escaped + '\\b' : escaped;
            const hits = (lower.match(new RegExp(pattern, 'g')) || []).length;
            score += hits;
        }
    }
    catch { /* binary or unreadable */ }
    return score;
}
function walkWorkspace(dir, maxFiles = 800) {
    const results = [];
    const stack = [dir];
    const workspaceRoot = path.resolve(dir);
    while (stack.length > 0 && results.length < maxFiles) {
        const current = stack.pop();
        if (!current)
            continue;
        let entries;
        try {
            entries = fs.readdirSync(current, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            // Hard cap enforced inside inner loop (fixes > 800 overshoot)
            if (results.length >= maxFiles)
                break;
            if (IGNORED_DIRS.has(entry.name))
                continue;
            const fullPath = path.join(current, entry.name);
            // Security: skip symlinks that escape workspace root (fixes 8.2)
            if (entry.isSymbolicLink()) {
                try {
                    const real = fs.realpathSync(fullPath);
                    if (!real.startsWith(workspaceRoot + path.sep) && real !== workspaceRoot)
                        continue;
                }
                catch {
                    continue;
                }
            }
            if (entry.isDirectory()) {
                stack.push(fullPath);
            }
            else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
                results.push(fullPath);
            }
        }
    }
    return results;
}
/**
 * Score and rank workspace files by relevance to the given prompt.
 * Returns the top N files with path, score, and content snippet.
 */
export function buildSemanticContext(workspacePath, prompt, topN = 12) {
    if (!workspacePath || !fs.existsSync(workspacePath)) {
        return { topFiles: [], totalFilesScanned: 0, keywords: [] };
    }
    const keywords = extractKeywords(prompt);
    if (keywords.length === 0) {
        return { topFiles: [], totalFilesScanned: 0, keywords: [] };
    }
    const files = walkWorkspace(workspacePath);
    const scored = files
        .map((f) => ({ filePath: f, score: scoreFile(f, keywords) }))
        .filter((f) => f.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, topN);
    const topFiles = scored.map(({ filePath, score }) => {
        let snippet = '';
        try {
            snippet = fs.readFileSync(filePath, 'utf-8').slice(0, 600);
        }
        catch { /* ignore */ }
        return {
            path: path.relative(workspacePath, filePath).replace(/\\/g, '/'),
            score,
            snippet,
        };
    });
    return { topFiles, totalFilesScanned: files.length, keywords };
}
