import path from 'node:path';
import { existsSync, readFileSync } from 'node:fs';
import { z } from 'zod';
import { CodeCapabilitiesSchema } from './types.js';
export const ParserTypeSchema = z.enum(['typescript', 'vue', 'python', 'go', 'rust', 'generic']);
export const TreeSitterDescriptorSchema = z.object({
    grammar: z.string(),
    functionNodeTypes: z.array(z.string()),
    classNodeTypes: z.array(z.string()),
    callNodeTypes: z.array(z.string()),
});
export const LanguageSpecSchema = z.object({
    name: z.string(),
    extensions: z.array(z.string()),
    parser: ParserTypeSchema,
    capabilities: CodeCapabilitiesSchema,
    treeSitter: TreeSitterDescriptorSchema.optional(),
});
const COMMON_CALL_NODES = ['call_expression', 'method_invocation'];
const COMMON_FUNCTION_NODES = ['function_definition', 'function_declaration', 'method_declaration'];
const COMMON_CLASS_NODES = ['class_definition', 'class_declaration', 'interface_declaration'];
export const LANGUAGE_SPECS = [
    {
        name: 'javascript',
        extensions: ['js', 'jsx', 'mjs', 'cjs', 'es6'],
        parser: 'typescript',
        capabilities: { definitions: true, imports: true, calls: true, inheritance: true, content: true },
    },
    {
        // `.mts`/`.cts` are first-class TypeScript sources that the diff layer
        // already recognized (`inferLanguage`) but discovery never indexed.
        name: 'typescript',
        extensions: ['ts', 'tsx', 'mts', 'cts'],
        parser: 'typescript',
        capabilities: { definitions: true, imports: true, calls: true, inheritance: true, content: true },
    },
    {
        name: 'vue',
        extensions: ['vue'],
        parser: 'vue',
        capabilities: {
            definitions: true,
            imports: true,
            calls: true,
            inheritance: true,
            content: true,
            templateUses: true,
        },
    },
    {
        name: 'python',
        extensions: ['py'],
        parser: 'python',
        capabilities: { definitions: true, imports: true, calls: true, inheritance: true, content: true },
        treeSitter: {
            grammar: 'python',
            functionNodeTypes: ['function_definition'],
            classNodeTypes: ['class_definition'],
            callNodeTypes: ['call'],
        },
    },
    {
        name: 'go',
        extensions: ['go'],
        parser: 'go',
        capabilities: { definitions: true, imports: true, calls: true, inheritance: false, content: true },
        treeSitter: {
            grammar: 'go',
            functionNodeTypes: ['function_declaration', 'method_declaration'],
            classNodeTypes: ['type_declaration'],
            callNodeTypes: ['call_expression'],
        },
    },
    {
        name: 'rust',
        extensions: ['rs'],
        parser: 'rust',
        capabilities: { definitions: true, imports: true, calls: true, inheritance: false, content: true },
        treeSitter: {
            grammar: 'rust',
            functionNodeTypes: ['function_item'],
            classNodeTypes: ['struct_item', 'enum_item', 'trait_item'],
            callNodeTypes: ['call_expression'],
        },
    },
    {
        // Extensions that map onto an already-vendored grammar. Deliberately
        // excluded: `.pyi` (stubs mirror their `.py` declarations, so indexing both
        // would make ordinary Python symbol lookups ambiguous) and `.inc`/`.module`
        // (only PHP by Drupal convention; they name other languages elsewhere).
        name: 'generic',
        extensions: [
            'java',
            'rb',
            'php',
            'phtml',
            'c',
            'h',
            'hpp',
            'hh',
            'hxx',
            'cpp',
            'cc',
            'cxx',
            'cs',
            'kt',
            'kts',
            'scala',
            'sc',
            'sbt',
            'swift',
        ],
        parser: 'generic',
        capabilities: { definitions: true, imports: true, calls: true, inheritance: true, content: true },
    },
];
const EXTENSION_TO_SPEC = new Map();
for (const spec of LANGUAGE_SPECS) {
    for (const extension of spec.extensions) {
        EXTENSION_TO_SPEC.set(extension, spec);
    }
}
export const CODE_EXTENSIONS = [...EXTENSION_TO_SPEC.keys()].sort();
export function getLanguageSpec(filePath) {
    const extension = path.extname(filePath).toLowerCase().replace('.', '');
    const spec = EXTENSION_TO_SPEC.get(extension);
    if (!spec)
        return null;
    return withExtensionTreeSitter(spec, extension);
}
function withExtensionTreeSitter(spec, extension) {
    if (spec.name !== 'generic')
        return spec;
    const descriptor = treeSitterDescriptorForExtension(extension);
    if (!descriptor)
        return spec;
    const nameByGrammar = {
        java: 'java',
        ruby: 'ruby',
        php: 'php',
        c: 'c',
        cpp: 'cpp',
        'c-sharp': 'csharp',
        kotlin: 'kotlin',
        scala: 'scala',
        swift: 'swift',
    };
    return { ...spec, name: nameByGrammar[descriptor.grammar] ?? descriptor.grammar, treeSitter: descriptor };
}
function treeSitterDescriptorForExtension(extension) {
    switch (extension) {
        case 'java':
            return {
                grammar: 'java',
                functionNodeTypes: ['method_declaration', 'constructor_declaration'],
                classNodeTypes: ['class_declaration', 'interface_declaration', 'enum_declaration'],
                callNodeTypes: ['method_invocation', 'object_creation_expression'],
            };
        case 'rb':
            return {
                grammar: 'ruby',
                functionNodeTypes: ['method', 'singleton_method'],
                classNodeTypes: ['class', 'module'],
                callNodeTypes: ['call', 'method_call', 'command'],
            };
        case 'php':
        case 'phtml':
            return {
                grammar: 'php',
                functionNodeTypes: ['function_definition', 'method_declaration'],
                classNodeTypes: ['class_declaration', 'interface_declaration', 'trait_declaration'],
                callNodeTypes: ['function_call_expression', 'member_call_expression', 'scoped_call_expression'],
            };
        case 'c':
        case 'h':
            return {
                grammar: 'c',
                functionNodeTypes: ['function_definition'],
                classNodeTypes: ['struct_specifier', 'enum_specifier'],
                callNodeTypes: ['call_expression'],
            };
        case 'hpp':
        case 'hh':
        case 'hxx':
        case 'cpp':
        case 'cc':
        case 'cxx':
            return {
                grammar: 'cpp',
                functionNodeTypes: ['function_definition'],
                classNodeTypes: ['class_specifier', 'struct_specifier', 'enum_specifier'],
                callNodeTypes: COMMON_CALL_NODES,
            };
        case 'cs':
            return {
                grammar: 'c-sharp',
                functionNodeTypes: ['method_declaration', 'constructor_declaration'],
                classNodeTypes: COMMON_CLASS_NODES,
                callNodeTypes: ['invocation_expression', 'object_creation_expression'],
            };
        case 'kt':
        case 'kts':
            return {
                grammar: 'kotlin',
                functionNodeTypes: ['function_declaration'],
                classNodeTypes: ['class_declaration', 'object_declaration'],
                callNodeTypes: ['call_expression'],
            };
        case 'scala':
        case 'sc':
        case 'sbt':
            return {
                grammar: 'scala',
                functionNodeTypes: ['function_definition'],
                classNodeTypes: ['class_definition', 'object_definition', 'trait_definition'],
                callNodeTypes: ['call_expression'],
            };
        case 'swift':
            return {
                grammar: 'swift',
                functionNodeTypes: ['function_declaration'],
                classNodeTypes: ['class_declaration', 'protocol_declaration', 'struct_declaration', 'enum_declaration'],
                callNodeTypes: ['call_expression'],
            };
        default:
            return undefined;
    }
}
export function getModuleName(repoRoot, filePath) {
    const relative = normalizePath(path.relative(repoRoot, filePath));
    return relative.replace(/\.[^.]+$/, '');
}
export function normalizePath(value) {
    return value.split(path.sep).join('/');
}
export function isLocalSpecifier(specifier) {
    return specifier.startsWith('.') || specifier.startsWith('/');
}
export function stripImportSuffix(specifier) {
    return specifier.replace(/[?#].*$/, '');
}
const ASSET_EXTENSIONS = new Set([
    'css',
    'scss',
    'sass',
    'less',
    'svg',
    'png',
    'jpg',
    'jpeg',
    'gif',
    'webp',
    'ico',
    'wasm',
]);
export function isAssetSpecifier(specifier) {
    const extension = path.extname(stripImportSuffix(specifier)).toLowerCase().replace('.', '');
    return extension.length > 0 && ASSET_EXTENSIONS.has(extension);
}
// NodeNext/ESM TypeScript specifiers name the emitted file (`./mod.js`), but
// the source on disk is `./mod.ts`/`./mod.tsx`. Remap so local imports in such
// projects resolve to their sources instead of dangling as unresolved edges.
// The literal candidate is still tried first, so a real .js file wins.
const JS_TO_TS_EXTENSION_REMAP = {
    '.js': ['.ts', '.tsx'],
    '.jsx': ['.tsx'],
    '.mjs': ['.mts', '.ts'],
    '.cjs': ['.cts', '.ts'],
};
function resolveAsCodePath(candidateBase) {
    const candidates = new Set([candidateBase]);
    const extension = path.extname(candidateBase).toLowerCase();
    const remappedExtensions = JS_TO_TS_EXTENSION_REMAP[extension];
    if (remappedExtensions) {
        const stem = candidateBase.slice(0, -extension.length);
        for (const remapped of remappedExtensions) {
            candidates.add(`${stem}${remapped}`);
        }
    }
    for (const candidate of [
        `${candidateBase}.ts`,
        `${candidateBase}.tsx`,
        `${candidateBase}.mts`,
        `${candidateBase}.cts`,
        `${candidateBase}.vue`,
        `${candidateBase}.js`,
        `${candidateBase}.jsx`,
        `${candidateBase}.mjs`,
        `${candidateBase}.cjs`,
        `${candidateBase}.py`,
        `${candidateBase}.go`,
        `${candidateBase}.rs`,
        `${candidateBase}.java`,
        `${candidateBase}.rb`,
        `${candidateBase}.php`,
        `${candidateBase}.c`,
        `${candidateBase}.cpp`,
        `${candidateBase}.h`,
        `${candidateBase}.hpp`,
        path.join(candidateBase, 'index.ts'),
        path.join(candidateBase, 'index.tsx'),
        path.join(candidateBase, 'index.vue'),
        path.join(candidateBase, 'index.js'),
        path.join(candidateBase, 'index.jsx'),
        path.join(candidateBase, 'index.py'),
    ]) {
        candidates.add(candidate);
    }
    for (const candidate of candidates) {
        const spec = getLanguageSpec(candidate);
        if (spec && existsSync(candidate))
            return candidate;
    }
    return undefined;
}
const tsConfigCache = new Map();
/**
 * Drop memoized path mappings after a tsconfig/jsconfig content change.
 * Callers pass a repository root so unrelated concurrent indexes keep their
 * warm entries; omitting it is reserved for tests and process-wide resets.
 */
export function invalidateTsConfigResolutionCache(repoRoot) {
    if (repoRoot === undefined) {
        tsConfigCache.clear();
        return;
    }
    tsConfigCache.delete(path.resolve(repoRoot));
}
function readTsConfigResolution(repoRoot) {
    const normalizedRoot = path.resolve(repoRoot);
    const cached = tsConfigCache.get(normalizedRoot);
    if (cached !== undefined)
        return cached;
    const candidates = ['tsconfig.json', 'jsconfig.json'].map(name => path.join(normalizedRoot, name));
    for (const filePath of candidates) {
        if (!existsSync(filePath))
            continue;
        try {
            const parsed = JSON.parse(readFileSync(filePath, 'utf8'));
            const compilerOptions = parsed.compilerOptions ?? {};
            const baseUrlValue = typeof compilerOptions.baseUrl === 'string' ? compilerOptions.baseUrl : '.';
            const pathsValue = compilerOptions.paths && typeof compilerOptions.paths === 'object'
                ? compilerOptions.paths
                : {};
            const paths = Object.entries(pathsValue)
                .filter((entry) => Array.isArray(entry[1]) && entry[1].every(value => typeof value === 'string'))
                .map(([pattern, targets]) => ({ pattern, targets }));
            const resolution = {
                baseUrl: path.resolve(repoRoot, baseUrlValue),
                paths,
            };
            tsConfigCache.set(normalizedRoot, resolution);
            return resolution;
        }
        catch {
            break;
        }
    }
    tsConfigCache.set(normalizedRoot, null);
    return null;
}
function matchTsConfigPattern(pattern, specifier) {
    if (!pattern.includes('*'))
        return pattern === specifier ? '' : null;
    const [prefix, suffix = ''] = pattern.split('*');
    if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix))
        return null;
    return specifier.slice(prefix.length, specifier.length - suffix.length);
}
export function resolveTsconfigImport(repoRoot, specifier) {
    const cleanSpecifier = stripImportSuffix(specifier);
    if (isLocalSpecifier(cleanSpecifier))
        return undefined;
    const config = readTsConfigResolution(repoRoot);
    if (!config)
        return undefined;
    for (const rule of config.paths) {
        const starValue = matchTsConfigPattern(rule.pattern, cleanSpecifier);
        if (starValue == null)
            continue;
        for (const target of rule.targets) {
            const mapped = target.includes('*') ? target.replace('*', starValue) : target;
            const resolved = resolveAsCodePath(path.resolve(config.baseUrl, mapped));
            if (resolved)
                return resolved;
        }
    }
    return resolveAsCodePath(path.resolve(config.baseUrl, cleanSpecifier));
}
export function resolveLocalImport(repoRoot, fromFile, specifier) {
    const fromDir = path.dirname(fromFile);
    const cleanSpecifier = stripImportSuffix(specifier);
    const candidateBase = path.resolve(cleanSpecifier.startsWith('/') ? repoRoot : fromDir, cleanSpecifier.startsWith('/') ? `.${cleanSpecifier}` : cleanSpecifier);
    return resolveAsCodePath(candidateBase);
}
export function resolvePythonImport(repoRoot, fromFile, specifier) {
    const resolveFrom = (baseDir, moduleName) => {
        const dottedPath = moduleName.replace(/\./g, '/');
        const moduleBase = path.resolve(baseDir, dottedPath || '.');
        const moduleFile = `${moduleBase}.py`;
        if (getLanguageSpec(moduleFile)?.name === 'python' && existsSync(moduleFile))
            return moduleFile;
        const packageFile = path.join(moduleBase, '__init__.py');
        if (getLanguageSpec(packageFile)?.name === 'python' && existsSync(packageFile))
            return packageFile;
        return undefined;
    };
    const relativePrefix = specifier.match(/^\.+/)?.[0] ?? '';
    if (relativePrefix) {
        // One leading dot names the importing file's package; each additional dot
        // moves to its parent. Converting `.lib` to `/lib` accidentally turned it
        // into a repository-root absolute path and made absolute/relative imports
        // indistinguishable.
        let packageDir = path.dirname(fromFile);
        for (let level = 1; level < relativePrefix.length; level++)
            packageDir = path.dirname(packageDir);
        return resolveFrom(packageDir, specifier.slice(relativePrefix.length));
    }
    // Python 3 bare imports are absolute. A sibling module is selected only by
    // an explicit leading dot; otherwise resolve from the repository import root.
    return resolveFrom(repoRoot, specifier);
}
//# sourceMappingURL=languages.js.map