/**
 * X-Ray Directory Scanner
 *
 * Walks a directory tree, scans each file, checks package.json if present,
 * and aggregates findings into a single XRayResult.
 */
import fs from 'fs';
import path from 'path';
import { scanFile } from './file-scanner.js';
import { calculateTrustScore } from './trust-score.js';
// ── Constants ───────────────────────────────────────────────
/** Directories to skip when walking. */
const SKIP_DIRS = new Set([
    'node_modules', '.git', '.svn', '.hg', 'dist', 'build', '.next',
    '__pycache__', '.tox', '.venv', 'venv', '.cache', 'coverage',
    'Caches', 'CacheStorage', 'IndexedDB', 'GPUCache',
]);
/**
 * Hidden directories worth scanning. These hold agent-instruction and CI
 * surfaces (skills, slash commands, settings hooks, workflow YAML, editor
 * tasks) — the primary prompt-injection / persistence vectors in an AI-agent
 * codebase. By default `walkDir` skips ALL dot-directories; this allow-list
 * carves out the ones X-Ray must inspect. `SKIP_DIRS` always wins, so `.git`
 * et al. stay excluded even though they're hidden.
 *
 * Shared with watch mode (`watch.ts` imports this) so a scan and a watch agree
 * on exactly which hidden dirs are in scope — one source of truth.
 */
export const ALLOW_HIDDEN_DIRS = new Set([
    '.github', '.claude', '.cursor', '.codex', '.vscode', '.agents', '.openclaw',
]);
/**
 * Whether to descend into a directory while walking. Recurse when it's not in
 * the skip-list AND it's either not hidden or explicitly allow-listed. Exported
 * so watch mode mirrors the identical rule.
 */
export function shouldWalkDir(name) {
    return !SKIP_DIRS.has(name) && (!name.startsWith('.') || ALLOW_HIDDEN_DIRS.has(name));
}
/** Absolute path prefixes to never scan — system/OS files that always produce false positives. */
const SKIP_PATH_PREFIXES = [
    '/System/',
    '/Library/',
    '/usr/',
    '/bin/',
    '/sbin/',
    '/private/var/',
    '/System/Volumes/Preboot/',
    '/System/Volumes/Data/private/',
];
/** Path segments that indicate non-code data we should skip. */
const SKIP_PATH_SEGMENTS = [
    '/Library/Caches/',
    '/Library/Application Support/Google/Chrome/',
    '/Library/Application Support/Firefox/',
    '/Library/Application Support/Arc/',
    '/Library/Containers/',
    '/Library/News/',
    '/Library/Mail/',
    '/Library/Messages/',
    '/Library/Safari/',
    '/Library/Cookies/',
    '/Library/Saved Application State/',
    '/Library/WebKit/',
    '/.Trash/',
];
/** Maximum number of files to scan per directory. */
const MAX_FILES = 5000;
/** Check if a full file path should be excluded from scanning. */
function isExcludedPath(filePath) {
    for (const prefix of SKIP_PATH_PREFIXES) {
        if (filePath.startsWith(prefix))
            return true;
    }
    for (const segment of SKIP_PATH_SEGMENTS) {
        if (filePath.includes(segment))
            return true;
    }
    return false;
}
// ── Directory walker ────────────────────────────────────────
function walkDir(dirPath, files, depth = 0) {
    if (depth > 20 || files.length >= MAX_FILES)
        return;
    // Skip entire system/cache directory trees early
    if (isExcludedPath(dirPath + '/'))
        return;
    let entries;
    try {
        entries = fs.readdirSync(dirPath, { withFileTypes: true });
    }
    catch {
        return;
    }
    for (const entry of entries) {
        if (files.length >= MAX_FILES)
            break;
        const fullPath = path.join(dirPath, entry.name);
        if (entry.isDirectory()) {
            if (shouldWalkDir(entry.name)) {
                walkDir(fullPath, files, depth + 1);
            }
        }
        else if (entry.isFile()) {
            if (!isExcludedPath(fullPath)) {
                files.push(fullPath);
            }
        }
    }
}
// ── Public API ──────────────────────────────────────────────
/**
 * Scan an entire directory for X-Ray findings.
 *
 * @param dirPath - Path to the directory to scan
 * @param deep - If true, performs deeper analysis (Pro feature)
 */
export async function scanDirectory(dirPath, deep) {
    const startTime = Date.now();
    const allFindings = [];
    // Collect files
    const files = [];
    walkDir(dirPath, files);
    // Scan each file
    for (const file of files) {
        const findings = await scanFile(file, deep);
        allFindings.push(...findings);
    }
    // Check for package.json at root
    const pkgJsonPath = path.join(dirPath, 'package.json');
    if (fs.existsSync(pkgJsonPath) && !files.includes(pkgJsonPath)) {
        const findings = await scanFile(pkgJsonPath, deep);
        allFindings.push(...findings);
    }
    const { score, riskLevel } = calculateTrustScore(allFindings);
    return {
        target: dirPath,
        trustScore: score,
        riskLevel,
        findings: allFindings,
        filesScanned: files.length,
        scannedAt: new Date(),
        deepScan: deep,
    };
}
