// engine/scan-project/module-discoverer.ts — Detects module boundaries in a project (SPEC-134)
// Language-agnostic: supports monorepo, package-based, framework-based, and directory-based detection.
import { readFile, readdir, stat } from 'node:fs/promises';
import { join, relative, basename, dirname } from 'node:path';
import { glob } from 'glob';
import { isNativeActive, fastFindFilesByName } from '../core-bridge.js';
// === Constants ===
const IGNORE_PATTERNS = [
    'node_modules',
    'dist',
    'build',
    '.git',
    '.next',
    '__pycache__',
    'target',
    '.venv',
    'venv',
    'vendor',
    'coverage',
    '.cache',
];
const SOURCE_EXTENSIONS = '{ts,js,tsx,jsx,py,go,rs,java,kt,rb,cs,swift,dart}';
const LANGUAGE_EXTENSION_MAP = {
    ts: 'TypeScript',
    tsx: 'TypeScript',
    js: 'JavaScript',
    jsx: 'JavaScript',
    py: 'Python',
    go: 'Go',
    rs: 'Rust',
    java: 'Java',
    kt: 'Kotlin',
    rb: 'Ruby',
    cs: 'C#',
    swift: 'Swift',
    dart: 'Dart',
};
const PACKAGE_MANIFESTS = [
    'package.json',
    'go.mod',
    'Cargo.toml',
    'pyproject.toml',
    'requirements.txt',
];
// === Helpers ===
/** Returns true if a path segment should be ignored */
function isIgnored(segment) {
    return IGNORE_PATTERNS.includes(segment) || segment.startsWith('.');
}
/** Counts source files in a directory using glob */
async function countSourceFiles(dirPath) {
    const pattern = `**/*.${SOURCE_EXTENSIONS}`;
    const files = await glob(pattern, {
        cwd: dirPath,
        ignore: IGNORE_PATTERNS.map((p) => `**/${p}/**`),
        nodir: true,
    });
    return files.length;
}
/** Detects dominant language from file extensions in a directory */
async function detectLanguage(dirPath) {
    const pattern = `**/*.${SOURCE_EXTENSIONS}`;
    const files = await glob(pattern, {
        cwd: dirPath,
        ignore: IGNORE_PATTERNS.map((p) => `**/${p}/**`),
        nodir: true,
    });
    const counts = {};
    for (const file of files) {
        const ext = file.split('.').pop() ?? '';
        counts[ext] = (counts[ext] ?? 0) + 1;
    }
    let dominant = 'unknown';
    let max = 0;
    for (const [ext, count] of Object.entries(counts)) {
        if (count > max) {
            max = count;
            dominant = LANGUAGE_EXTENSION_MAP[ext] ?? ext;
        }
    }
    return dominant;
}
/** Returns immediate subdirectories of a path, excluding ignored ones */
async function getSubdirs(dirPath) {
    let entries;
    try {
        entries = await readdir(dirPath);
    }
    catch {
        return [];
    }
    const subdirs = [];
    for (const entry of entries) {
        if (isIgnored(entry)) {
            continue;
        }
        try {
            const fullPath = join(dirPath, entry);
            const s = await stat(fullPath);
            if (s.isDirectory()) {
                subdirs.push(entry);
            }
        }
        catch {
            // skip unreadable entries
        }
    }
    return subdirs;
}
/** Checks if a file exists at the given path */
async function fileExists(filePath) {
    try {
        await stat(filePath);
        return true;
    }
    catch {
        return false;
    }
}
/** Reads a file safely, returns empty string on error */
async function safeRead(filePath) {
    try {
        return await readFile(filePath, 'utf8');
    }
    catch {
        return '';
    }
}
// === Manual module resolution ===
async function resolveManualModules(projectPath, manualModules) {
    const modules = [];
    for (const modPath of manualModules) {
        const fullPath = join(projectPath, modPath);
        const [estimatedFiles, language] = await Promise.all([
            countSourceFiles(fullPath),
            detectLanguage(fullPath),
        ]);
        modules.push({
            name: basename(modPath),
            path: modPath,
            estimatedFiles,
            language,
            detectionMethod: 'manual',
        });
    }
    return modules;
}
// === Monorepo detection ===
/** Parses workspace globs from pnpm-workspace.yaml content */
function parsePnpmWorkspaces(content) {
    const lines = content.split('\n');
    const globs = [];
    let inPackages = false;
    for (const line of lines) {
        if (line.trim() === 'packages:') {
            inPackages = true;
            continue;
        }
        if (inPackages) {
            const match = /^\s+-\s+['"]?([^'"]+)['"]?/.exec(line);
            if (match?.[1]) {
                globs.push(match[1]);
            }
            else if (line.trim() && !line.startsWith(' ') && !line.startsWith('\t')) {
                break;
            }
        }
    }
    return globs;
}
/** Extracts package globs from lerna.json or turbo.json content */
function parseJsonWorkspaces(content) {
    try {
        const parsed = JSON.parse(content);
        const packages = parsed.packages;
        if (Array.isArray(packages)) {
            return packages.filter((p) => typeof p === 'string');
        }
        const workspaces = parsed.workspaces;
        if (Array.isArray(workspaces)) {
            return workspaces.filter((p) => typeof p === 'string');
        }
    }
    catch {
        // ignore parse errors
    }
    return [];
}
async function detectMonorepo(projectPath) {
    const pnpmWorkspace = join(projectPath, 'pnpm-workspace.yaml');
    const lernaJson = join(projectPath, 'lerna.json');
    const turboJson = join(projectPath, 'turbo.json');
    let workspaceGlobs = [];
    if (await fileExists(pnpmWorkspace)) {
        const content = await safeRead(pnpmWorkspace);
        workspaceGlobs = parsePnpmWorkspaces(content);
    }
    else if (await fileExists(lernaJson)) {
        const content = await safeRead(lernaJson);
        workspaceGlobs = parseJsonWorkspaces(content);
    }
    else if (await fileExists(turboJson)) {
        // turbo.json itself doesn't define packages; fall back to package.json workspaces
        const pkgJson = join(projectPath, 'package.json');
        const content = await safeRead(pkgJson);
        workspaceGlobs = parseJsonWorkspaces(content);
    }
    if (workspaceGlobs.length === 0) {
        return null;
    }
    const resolved = [];
    if (isNativeActive()) {
        // Rust: optimized directory resolution for monorepo patterns
        const pkgJsonPaths = fastFindFilesByName(projectPath, ['package.json']);
        // Filter directories that match the workspace globs (simplified heuristic).
        const dirs = pkgJsonPaths
            .map((p) => dirname(p))
            .map((p) => relative(projectPath, p))
            .filter((rel) => rel !== '' && !isIgnored(rel.split('/')[0] ?? ''));
        // In a real implementation we would match against workspaceGlobs exactly,
        // but for speed we take all dirs with package.json that aren't ignored.
        resolved.push(...dirs);
    }
    if (resolved.length === 0) {
        for (const pattern of workspaceGlobs) {
            // Append '/' to match only directories (glob treats trailing slash as dir-only)
            const dirPattern = pattern.endsWith('/') ? pattern : `${pattern}/`;
            const matches = await glob(dirPattern, {
                cwd: projectPath,
                ignore: IGNORE_PATTERNS.map((p) => `**/${p}/**`),
            });
            // Strip trailing slash glob may add
            resolved.push(...matches.map((m) => m.replace(/\/$/, '')));
        }
    }
    if (resolved.length === 0) {
        return null;
    }
    const modules = await Promise.all(resolved.map(async (relPath) => {
        const fullPath = join(projectPath, relPath);
        const [estimatedFiles, language] = await Promise.all([
            countSourceFiles(fullPath),
            detectLanguage(fullPath),
        ]);
        return {
            name: basename(relPath),
            path: relPath,
            estimatedFiles,
            language,
            detectionMethod: 'monorepo',
        };
    }));
    return modules;
}
// === Package-based detection ===
async function detectPackageBased(projectPath) {
    if (isNativeActive()) {
        const manifests = fastFindFilesByName(projectPath, PACKAGE_MANIFESTS);
        if (manifests.length > 0) {
            const modules = [];
            const seen = new Set();
            for (const m of manifests) {
                const rel = relative(projectPath, dirname(m));
                if (rel === '' || rel.includes('/') || isIgnored(rel) || seen.has(rel)) {
                    continue;
                }
                seen.add(rel);
                const [estimatedFiles, language] = await Promise.all([
                    countSourceFiles(join(projectPath, rel)),
                    detectLanguage(join(projectPath, rel)),
                ]);
                modules.push({
                    name: rel,
                    path: rel,
                    estimatedFiles,
                    language,
                    detectionMethod: 'package',
                });
            }
            if (modules.length > 0) {
                return modules;
            }
        }
    }
    const subdirs = await getSubdirs(projectPath);
    const modules = [];
    for (const subdir of subdirs) {
        const fullPath = join(projectPath, subdir);
        const hasManifest = await Promise.all(PACKAGE_MANIFESTS.map((m) => fileExists(join(fullPath, m))));
        if (hasManifest.some(Boolean)) {
            const [estimatedFiles, language] = await Promise.all([
                countSourceFiles(fullPath),
                detectLanguage(join(fullPath)),
            ]);
            modules.push({
                name: subdir,
                path: subdir,
                estimatedFiles,
                language,
                detectionMethod: 'package',
            });
        }
    }
    return modules.length > 0 ? modules : null;
}
// === Framework-based detection ===
/** Detects Next.js modules (app/ or pages/) */
async function detectNextJs(projectPath) {
    const modules = [];
    for (const dir of ['app', 'pages']) {
        const fullPath = join(projectPath, dir);
        if (await fileExists(fullPath)) {
            const [estimatedFiles, language] = await Promise.all([
                countSourceFiles(fullPath),
                detectLanguage(fullPath),
            ]);
            modules.push({
                name: dir,
                path: dir,
                estimatedFiles,
                language,
                detectionMethod: 'framework',
            });
        }
    }
    return modules;
}
/** Detects Django apps (dirs containing apps.py alongside manage.py) */
async function detectDjango(projectPath) {
    if (!(await fileExists(join(projectPath, 'manage.py')))) {
        return [];
    }
    const subdirs = await getSubdirs(projectPath);
    const modules = [];
    for (const subdir of subdirs) {
        const appsPy = join(projectPath, subdir, 'apps.py');
        if (await fileExists(appsPy)) {
            const fullPath = join(projectPath, subdir);
            const [estimatedFiles, language] = await Promise.all([
                countSourceFiles(fullPath),
                detectLanguage(fullPath),
            ]);
            modules.push({
                name: subdir,
                path: subdir,
                estimatedFiles,
                language,
                detectionMethod: 'framework',
            });
        }
    }
    return modules;
}
/** Detects NestJS modules (dirs containing *.module.ts) */
async function detectNestJs(projectPath) {
    const srcPath = join(projectPath, 'src');
    const basePath = (await fileExists(srcPath)) ? srcPath : projectPath;
    const subdirs = await getSubdirs(basePath);
    const modules = [];
    for (const subdir of subdirs) {
        const fullPath = join(basePath, subdir);
        const nestModules = await glob('*.module.ts', { cwd: fullPath, nodir: true });
        if (nestModules.length > 0) {
            const relPath = relative(projectPath, fullPath);
            const [estimatedFiles, language] = await Promise.all([
                countSourceFiles(fullPath),
                detectLanguage(fullPath),
            ]);
            modules.push({
                name: subdir,
                path: relPath,
                estimatedFiles,
                language,
                detectionMethod: 'framework',
            });
        }
    }
    return modules;
}
async function detectFrameworkBased(projectPath) {
    const [nextModules, djangoModules, nestModules] = await Promise.all([
        detectNextJs(projectPath),
        detectDjango(projectPath),
        detectNestJs(projectPath),
    ]);
    const all = [...nextModules, ...djangoModules, ...nestModules];
    return all.length > 0 ? all : null;
}
// === Directory-based detection (fallback) ===
async function detectDirectoryBased(projectPath) {
    const srcPath = join(projectPath, 'src');
    const hasSrc = await fileExists(srcPath);
    const basePath = hasSrc ? srcPath : projectPath;
    const subdirs = await getSubdirs(basePath);
    const modules = await Promise.all(subdirs.map(async (subdir) => {
        const fullPath = join(basePath, subdir);
        const relPath = relative(projectPath, fullPath);
        const [estimatedFiles, language] = await Promise.all([
            countSourceFiles(fullPath),
            detectLanguage(fullPath),
        ]);
        return {
            name: subdir,
            path: relPath,
            estimatedFiles,
            language,
            detectionMethod: 'directory',
        };
    }));
    return modules;
}
// === Public API ===
export async function discoverModules(projectPath, manualModules) {
    if (manualModules && manualModules.length > 0) {
        return resolveManualModules(projectPath, manualModules);
    }
    const monorepoModules = await detectMonorepo(projectPath);
    if (monorepoModules) {
        return monorepoModules;
    }
    const packageModules = await detectPackageBased(projectPath);
    if (packageModules) {
        return packageModules;
    }
    const frameworkModules = await detectFrameworkBased(projectPath);
    if (frameworkModules) {
        return frameworkModules;
    }
    return detectDirectoryBased(projectPath);
}
//# sourceMappingURL=module-discoverer.js.map