import fg from 'fast-glob';
import { stat } from 'node:fs/promises';
import path from 'node:path';
import { z } from 'zod';
import { CODE_EXTENSIONS, getLanguageSpec, normalizePath } from './languages.js';
import { DEFAULT_EXCLUDE_DIRS, abortIfNeeded, buildExcludeDirGlobs, compareCodeUnits, createIgnoreMatcher, mapWithConcurrency, } from '../utils.js';
export const PreviewCodebaseSizeOptionsSchema = z.object({
    repoRoot: z.string(),
    excludeDir: z.array(z.string()).optional(),
    noGitignore: z.boolean().optional(),
    ignorePatterns: z.array(z.string()).optional(),
    overlayIgnorePatterns: z.array(z.string()).optional(),
    maxFiles: z.number().int().positive().optional(),
    maxBytes: z.number().int().positive().optional(),
});
const CODE_EXTENSION_SET = new Set(CODE_EXTENSIONS);
export async function previewCodebaseSize(options) {
    const repoRoot = path.resolve(options.repoRoot);
    const excludeDir = options.excludeDir ?? [...DEFAULT_EXCLUDE_DIRS];
    abortIfNeeded(options.signal);
    const ignoreMatcher = await createIgnoreMatcher(repoRoot, {
        excludeDir,
        noGitignore: options.noGitignore,
        ignorePatterns: options.ignorePatterns,
        overlayIgnorePatterns: options.overlayIgnorePatterns,
    });
    const discovered = await fg('**/*', {
        cwd: repoRoot,
        absolute: true,
        onlyFiles: true,
        dot: false,
        followSymbolicLinks: false,
        ignore: buildExcludeDirGlobs(excludeDir),
        unique: true,
    });
    abortIfNeeded(options.signal);
    const nonIgnored = discovered.sort().filter(filePath => {
        const relativePath = normalizePath(path.relative(repoRoot, filePath));
        return !ignoreMatcher.ignores(relativePath);
    });
    abortIfNeeded(options.signal);
    // CODE_EXTENSIONS are lowercase; comparing the raw (case-sensitive) extension
    // mirrors the case-sensitive glob the index build discovers code with, so the
    // preview counts only files the build would actually index.
    const codeFilePaths = nonIgnored.filter(filePath => CODE_EXTENSION_SET.has(path.extname(filePath).replace('.', '')));
    const sized = await mapWithConcurrency(codeFilePaths, 32, async (filePath) => {
        try {
            const fileStat = await stat(filePath);
            return fileStat.isFile() ? { path: filePath, size: fileStat.size } : null;
        }
        catch {
            return null;
        }
    }, options.signal);
    const totalFiles = nonIgnored.length;
    let codeFiles = 0;
    let estimatedBytes = 0;
    const languages = new Map();
    for (const entry of sized) {
        if (!entry)
            continue;
        codeFiles++;
        estimatedBytes += entry.size;
        const language = getLanguageSpec(entry.path)?.name ?? path.extname(entry.path).replace('.', '');
        const current = languages.get(language) ?? { files: 0, bytes: 0 };
        current.files++;
        current.bytes += entry.size;
        languages.set(language, current);
    }
    const overFiles = options.maxFiles != null && codeFiles > options.maxFiles;
    const overBytes = options.maxBytes != null && estimatedBytes > options.maxBytes;
    return {
        repoRoot,
        totalFiles,
        codeFiles,
        estimatedBytes,
        languages: [...languages.entries()]
            .map(([language, counts]) => ({ language, ...counts }))
            .sort((left, right) => right.files - left.files || compareCodeUnits(left.language, right.language)),
        ...(overFiles || overBytes
            ? {
                exceedsBudget: {
                    maxFiles: options.maxFiles,
                    maxBytes: options.maxBytes,
                    codeFiles,
                    estimatedBytes,
                },
            }
            : {}),
    };
}
//# sourceMappingURL=preview.js.map