// engine/ai-cost-estimator/token-estimator.ts — Token count estimation logic.
// Calculates input tokens (spec + codebase + tools) and output tokens (code generation).
import { readFile, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
// ---------------------------------------------------------------------------
// Constants (overridden by config)
// ---------------------------------------------------------------------------
const DEFAULT_TOOL_DEFINITIONS_TOKENS = 4000;
const DEFAULT_CHARS_PER_TOKEN = 3.5;
const DEFAULT_AVG_LOC_CHARS = 45;
const DEFAULT_LOC_PER_AC = 50;
// ---------------------------------------------------------------------------
// Language detection
// ---------------------------------------------------------------------------
const LANGUAGE_FILE_EXTENSIONS = {
    typescript: ['.ts', '.tsx'],
    javascript: ['.js', '.jsx', '.mjs', '.cjs'],
    python: ['.py'],
    go: ['.go'],
    rust: ['.rs'],
    java: ['.java'],
    kotlin: ['.kt', '.kts'],
    swift: ['.swift'],
    ruby: ['.rb'],
    php: ['.php'],
    csharp: ['.cs'],
    dart: ['.dart'],
};
const LANGUAGE_CONFIG_FILES = {
    typescript: ['tsconfig.json', 'package.json'],
    python: ['pyproject.toml', 'setup.cfg', 'setup.py', 'requirements.txt'],
    go: ['go.mod'],
    rust: ['Cargo.toml'],
    java: ['pom.xml', 'build.gradle'],
    kotlin: ['build.gradle.kts'],
    swift: ['Package.swift'],
    ruby: ['Gemfile'],
    php: ['composer.json'],
    csharp: ['.csproj'],
    dart: ['pubspec.yaml'],
};
/**
 * Detects the primary programming language of a project by checking config files.
 * Falls back to scanning source file extensions.
 */
export async function detectProjectLanguage(projectPath) {
    // Check config file markers first (most reliable)
    for (const [lang, markers] of Object.entries(LANGUAGE_CONFIG_FILES)) {
        for (const marker of markers) {
            try {
                await stat(join(projectPath, marker));
                return lang;
            }
            catch {
                // File does not exist, continue
            }
        }
    }
    return 'default';
}
// ---------------------------------------------------------------------------
// Codebase scanning
// ---------------------------------------------------------------------------
const MAX_SCAN_DEPTH = 5;
const MAX_SCANNED_SOURCE_FILES = 200;
const EXCLUDED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '__pycache__', '.next']);
/** Recursively counts total characters in source files up to a given depth. */
async function countCodebaseChars(dirPath, depth, filesScanned = 0) {
    if (depth > MAX_SCAN_DEPTH) {
        return { chars: 0, files: filesScanned };
    }
    if (filesScanned >= MAX_SCANNED_SOURCE_FILES) {
        return { chars: 0, files: filesScanned };
    }
    let totalChars = 0;
    let totalFiles = filesScanned;
    let entries;
    try {
        entries = await readdir(dirPath, { withFileTypes: true, encoding: 'utf-8' });
    }
    catch {
        return { chars: 0, files: totalFiles };
    }
    for (const entry of entries) {
        const entryName = entry.name;
        if (EXCLUDED_DIRS.has(entryName)) {
            continue;
        }
        const fullPath = join(dirPath, entryName);
        if (entry.isDirectory()) {
            const nested = await countCodebaseChars(fullPath, depth + 1, totalFiles);
            totalChars += nested.chars;
            totalFiles = nested.files;
        }
        else if (entry.isFile() && isSourceFile(entryName)) {
            if (totalFiles >= MAX_SCANNED_SOURCE_FILES) {
                break;
            }
            try {
                const content = await readFile(fullPath, 'utf-8');
                totalChars += content.length;
                totalFiles++;
            }
            catch {
                // Skip unreadable files
            }
        }
    }
    return { chars: totalChars, files: totalFiles };
}
/** Returns true if the file extension is a known source file type. */
function isSourceFile(filename) {
    const ext = filename.slice(filename.lastIndexOf('.'));
    const allExtensions = Object.values(LANGUAGE_FILE_EXTENSIONS).flat();
    return allExtensions.includes(ext);
}
// ---------------------------------------------------------------------------
// Input token estimation
// ---------------------------------------------------------------------------
/**
 * Estimates input tokens for an AI agent session based on three components:
 * 1. Spec content (canonical spec.md)
 * 2. Codebase context (if projectPath is provided)
 * 3. MCP tool definitions overhead (fixed constant)
 */
export async function estimateInputTokens(specContent, projectPath, config) {
    const charsPerToken = config.constants.charsPerToken || DEFAULT_CHARS_PER_TOKEN;
    const toolDefinitionsTokens = config.constants.toolDefinitionsOverheadTokens || DEFAULT_TOOL_DEFINITIONS_TOKENS;
    // 1. Spec content tokens
    const specContentTokens = Math.ceil(specContent.length / charsPerToken);
    // 2. Codebase context tokens (optional)
    let codebaseContextTokens = 0;
    if (projectPath) {
        const { chars: codebaseChars } = await countCodebaseChars(projectPath, 0);
        codebaseContextTokens = Math.ceil(codebaseChars / charsPerToken);
    }
    const inputTokens = specContentTokens + codebaseContextTokens + toolDefinitionsTokens;
    return {
        inputTokens,
        specContentTokens,
        codebaseContextTokens,
        toolDefinitionsTokens,
    };
}
// ---------------------------------------------------------------------------
// Output token estimation
// ---------------------------------------------------------------------------
/**
 * Estimates output tokens (code to be generated) for a spec.
 *
 * Priority:
 * 1. Use LOC from existing estimate if available
 * 2. Fallback: number of ACs × DEFAULT_LOC_PER_AC
 * 3. Calibrate with usage.json average if available
 */
export function estimateOutputTokens(locToGenerate, language, config, usageAvgTokensPerCall) {
    const avgLOCChars = config.constants.avgLOCChars || DEFAULT_AVG_LOC_CHARS;
    // Look up the language token ratio from config
    const ratioKey = language.toLowerCase();
    const tokensPerChar = config.languageTokenRatios[ratioKey] ??
        config.languageTokenRatios.default ??
        DEFAULT_CHARS_PER_TOKEN;
    // Base estimate: LOC × chars/LOC × tokens/char
    let outputTokens = Math.ceil(locToGenerate * avgLOCChars * tokensPerChar);
    // Calibrate with usage data if available
    if (usageAvgTokensPerCall !== undefined && usageAvgTokensPerCall > 0) {
        // Blend: 70% formula estimate, 30% historical calibration
        outputTokens = Math.ceil(outputTokens * 0.7 + usageAvgTokensPerCall * 0.3);
    }
    return outputTokens;
}
/**
 * Estimates LOC to generate based on the number of acceptance criteria.
 * Used as a fallback when no existing estimate is available.
 */
export function estimateLocFromAcCount(acCount) {
    return acCount * DEFAULT_LOC_PER_AC;
}
// ---------------------------------------------------------------------------
// Full token estimate
// ---------------------------------------------------------------------------
/**
 * Produces a complete TokenEstimate for a spec.
 */
export async function buildTokenEstimate(specContent, locToGenerate, acCount, projectPath, config, usageAvgTokensPerCall) {
    // Detect language for token ratio lookup
    const language = projectPath ? await detectProjectLanguage(projectPath) : 'default';
    // Estimate input components
    const { inputTokens, specContentTokens, codebaseContextTokens, toolDefinitionsTokens } = await estimateInputTokens(specContent, projectPath, config);
    // Use provided LOC or fallback to AC-based estimate
    const effectiveLOC = locToGenerate > 0 ? locToGenerate : estimateLocFromAcCount(acCount);
    // Estimate output tokens
    const outputTokens = estimateOutputTokens(effectiveLOC, language, config, usageAvgTokensPerCall);
    return {
        inputTokens,
        outputTokens,
        specContentTokens,
        codebaseContextTokens,
        toolDefinitionsTokens,
        locToGenerate: effectiveLOC,
        language,
        calibratedFromUsage: usageAvgTokensPerCall !== undefined && usageAvgTokensPerCall > 0,
    };
}
//# sourceMappingURL=token-estimator.js.map