import * as fs from 'node:fs';
import * as path from 'node:path';
const IMPORT_PATTERNS = [
    // ES import: import ... from './foo'
    /(?:import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"])/g,
    // ES dynamic import: import('./foo')
    /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
    // CommonJS require: require('./foo')
    /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
];
const SOURCE_EXTENSIONS = new Set([
    '.ts',
    '.tsx',
    '.js',
    '.jsx',
    '.mjs',
    '.cjs',
    '.py',
    '.go',
    '.java',
    '.kt',
    '.rs',
    '.rb',
]);
const IGNORE_DIRS = new Set([
    'node_modules',
    '.git',
    'dist',
    'build',
    'out',
    '.next',
    '.nuxt',
    'coverage',
    'vendor',
    '__pycache__',
]);
const HUB_THRESHOLD = 10;
function collectSourceFiles(dir) {
    const results = [];
    const queue = [dir];
    while (queue.length > 0) {
        const current = queue.pop();
        if (current === undefined) {
            continue;
        }
        let entries;
        try {
            entries = fs.readdirSync(current, { withFileTypes: true });
        }
        catch {
            continue;
        }
        for (const entry of entries) {
            if (IGNORE_DIRS.has(entry.name)) {
                continue;
            }
            const full = path.join(current, entry.name);
            if (entry.isDirectory()) {
                queue.push(full);
            }
            else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
                results.push(full);
            }
        }
    }
    return results;
}
function extractImports(content) {
    const imports = [];
    for (const pattern of IMPORT_PATTERNS) {
        const regex = new RegExp(pattern.source, pattern.flags);
        let match;
        while ((match = regex.exec(content)) !== null) {
            const spec = match[1] ?? '';
            // Only internal (relative) imports
            if (spec.startsWith('.') || spec.startsWith('/')) {
                imports.push(spec);
            }
        }
    }
    return imports;
}
function resolveImport(fromFile, importSpec, allFiles) {
    const dir = path.dirname(fromFile);
    const resolved = path.resolve(dir, importSpec);
    // Try exact match
    if (allFiles.has(resolved)) {
        return resolved;
    }
    // Try with extensions
    for (const ext of SOURCE_EXTENSIONS) {
        const withExt = resolved.replace(/\.js$/, '') + ext;
        if (allFiles.has(withExt)) {
            return withExt;
        }
        // Without .js replacement
        if (allFiles.has(resolved + ext)) {
            return resolved + ext;
        }
    }
    // Try index files
    for (const ext of SOURCE_EXTENSIONS) {
        const indexFile = path.join(resolved, `index${ext}`);
        if (allFiles.has(indexFile)) {
            return indexFile;
        }
    }
    return undefined;
}
function detectCycles(adjacency) {
    const cycles = [];
    const visited = new Set();
    const inStack = new Set();
    const stack = [];
    function dfs(node) {
        if (inStack.has(node)) {
            const cycleStart = stack.indexOf(node);
            /* v8 ignore next 3 */
            if (cycleStart !== -1) {
                cycles.push(stack.slice(cycleStart));
            }
            return;
        }
        if (visited.has(node)) {
            return;
        }
        visited.add(node);
        inStack.add(node);
        stack.push(node);
        for (const neighbor of /* v8 ignore next */ adjacency.get(node) ?? []) {
            dfs(neighbor);
        }
        stack.pop();
        inStack.delete(node);
    }
    for (const node of adjacency.keys()) {
        dfs(node);
    }
    return cycles;
}
export function buildDependencyGraph(rootDir) {
    const files = collectSourceFiles(rootDir);
    const fileSet = new Set(files);
    const adjacency = new Map();
    const fanInCount = new Map();
    // Initialize all nodes
    for (const file of files) {
        const rel = path.relative(rootDir, file);
        adjacency.set(rel, []);
        fanInCount.set(rel, 0);
    }
    // Build adjacency
    for (const file of files) {
        const rel = path.relative(rootDir, file);
        let content;
        try {
            content = fs.readFileSync(file, 'utf-8');
        }
        catch {
            continue;
        }
        const imports = extractImports(content);
        const resolvedImports = [];
        for (const imp of imports) {
            const resolved = resolveImport(file, imp, fileSet);
            if (resolved) {
                const resolvedRel = path.relative(rootDir, resolved);
                if (resolvedRel !== rel) {
                    resolvedImports.push(resolvedRel);
                    fanInCount.set(resolvedRel, /* v8 ignore next */ (fanInCount.get(resolvedRel) ?? 0) + 1);
                }
            }
        }
        adjacency.set(rel, resolvedImports);
    }
    // Build nodes
    const nodes = [];
    for (const [file, imports] of adjacency) {
        nodes.push({
            file,
            imports,
            fanIn: /* v8 ignore next */ fanInCount.get(file) ?? 0,
            fanOut: imports.length,
        });
    }
    // Detect cycles
    const cycles = detectCycles(adjacency);
    // Identify hubs and orphans
    const hubs = nodes
        .filter((n) => n.fanIn > HUB_THRESHOLD)
        .map((n) => n.file)
        .sort();
    const orphans = nodes
        .filter((n) => n.fanIn === 0 && n.fanOut === 0)
        .map((n) => n.file)
        .sort();
    return { nodes, cycles, hubs, orphans };
}
//# sourceMappingURL=dependency-graph.js.map