"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.clearPathMappingsCache = clearPathMappingsCache;
exports.parseCodeFile = parseCodeFile;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const languages_1 = require("../../lib/parsers/languages");
const file_lang_1 = require("../../graphs/file-lang");
// Strip line and block comments from JSONC, preserving string contents.
function stripJsoncComments(text) {
    let result = '';
    let i = 0;
    while (i < text.length) {
        // String literal — copy verbatim
        if (text[i] === '"') {
            const start = i++;
            while (i < text.length && text[i] !== '"') {
                if (text[i] === '\\')
                    i++; // skip escaped char
                i++;
            }
            i++; // closing quote
            result += text.slice(start, i);
            // Line comment
        }
        else if (text[i] === '/' && text[i + 1] === '/') {
            while (i < text.length && text[i] !== '\n')
                i++;
            // Block comment
        }
        else if (text[i] === '/' && text[i + 1] === '*') {
            i += 2;
            while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
                i++;
            i += 2;
        }
        else {
            result += text[i++];
        }
    }
    return result;
}
// ---------------------------------------------------------------------------
// Import resolution — replaces ts-morph's getModuleSpecifierSourceFile()
// ---------------------------------------------------------------------------
const RESOLVE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'];
function hasFile(p) {
    try {
        return fs_1.default.statSync(p).isFile();
    }
    catch {
        return false;
    }
}
/** Try resolving a base path with extensions and index files. */
function tryResolve(base) {
    if (hasFile(base))
        return base;
    for (const ext of RESOLVE_EXTENSIONS) {
        if (hasFile(base + ext))
            return base + ext;
    }
    for (const ext of RESOLVE_EXTENSIONS) {
        const idx = path_1.default.join(base, 'index' + ext);
        if (hasFile(idx))
            return idx;
    }
    return null;
}
/** Resolve a relative import specifier to an absolute file path, or null. */
function resolveRelativeImport(fromFile, specifier) {
    const dir = path_1.default.dirname(fromFile);
    return tryResolve(path_1.default.resolve(dir, specifier));
}
/** Cache: directory → parsed path mappings (null = no tsconfig found up to root). */
const _pathMappings = new Map();
/** Clear cached path mappings (call between projects or on config change). */
function clearPathMappingsCache() { _pathMappings.clear(); }
/**
 * Find the nearest tsconfig.json / jsconfig.json walking up from `dir` to `root`.
 * Cached per directory — each directory remembers its resolved mappings.
 */
function findPathMappings(dir, root) {
    if (_pathMappings.has(dir))
        return _pathMappings.get(dir);
    // Try this directory
    const result = _parseTsConfig(dir);
    if (result) {
        _pathMappings.set(dir, result);
        return result;
    }
    // Walk up unless we've reached the project root
    const parent = path_1.default.dirname(dir);
    if (dir === root || parent === dir) {
        _pathMappings.set(dir, null);
        return null;
    }
    const parentResult = findPathMappings(parent, root);
    _pathMappings.set(dir, parentResult);
    return parentResult;
}
function _parseTsConfig(dir) {
    for (const name of ['tsconfig.json', 'jsconfig.json']) {
        const configPath = path_1.default.join(dir, name);
        if (!hasFile(configPath))
            continue;
        try {
            // Strip JSONC comments while preserving string contents
            const raw = stripJsoncComments(fs_1.default.readFileSync(configPath, 'utf-8'));
            const config = JSON.parse(raw);
            const compilerOptions = config.compilerOptions;
            if (!compilerOptions?.paths)
                continue;
            const baseUrl = compilerOptions.baseUrl
                ? path_1.default.resolve(dir, compilerOptions.baseUrl)
                : dir;
            const mappings = [];
            for (const [pattern, targets] of Object.entries(compilerOptions.paths)) {
                // Pattern like "@/*" → prefix "@/", or "utils/*" → prefix "utils/"
                const prefix = pattern.endsWith('/*') ? pattern.slice(0, -1) : pattern;
                const resolvedTargets = targets
                    .map(t => {
                    const target = t.endsWith('/*') ? t.slice(0, -1) : t;
                    return path_1.default.resolve(baseUrl, target);
                });
                mappings.push({ prefix, targets: resolvedTargets });
            }
            if (mappings.length > 0)
                return mappings;
        }
        catch {
            // Malformed config — skip
        }
    }
    return null;
}
/** Resolve a path-aliased import (e.g. @/lib/foo) using nearest tsconfig paths. */
function resolveAliasImport(specifier, fromFile, projectDir) {
    const fileDir = path_1.default.dirname(fromFile);
    const mappings = findPathMappings(fileDir, projectDir);
    if (!mappings)
        return null;
    for (const mapping of mappings) {
        if (specifier.startsWith(mapping.prefix)) {
            const rest = specifier.slice(mapping.prefix.length);
            for (const targetDir of mapping.targets) {
                const resolved = tryResolve(path_1.default.join(targetDir, rest));
                if (resolved)
                    return resolved;
            }
        }
    }
    return null;
}
// ---------------------------------------------------------------------------
// Main parser
// ---------------------------------------------------------------------------
async function parseCodeFile(absolutePath, codeDir, mtime) {
    const fileId = path_1.default.relative(codeDir, absolutePath);
    // Determine language from file extension
    const ext = path_1.default.extname(absolutePath);
    const language = (0, file_lang_1.getLanguage)(ext);
    if (!language || !(0, languages_1.isLanguageSupported)(language)) {
        // Unsupported language — return file-only node, no symbols
        return {
            fileId,
            mtime,
            nodes: [{
                    id: fileId,
                    attrs: makeFileAttrs(fileId, '', '', 1, mtime),
                }],
            edges: [],
        };
    }
    const source = fs_1.default.readFileSync(absolutePath, 'utf-8');
    const tree = await (0, languages_1.parseSource)(source, language);
    if (!tree) {
        return {
            fileId,
            mtime,
            nodes: [{
                    id: fileId,
                    attrs: makeFileAttrs(fileId, '', '', 1, mtime),
                }],
            edges: [],
        };
    }
    const rootNode = tree.rootNode;
    const mapper = (0, languages_1.getMapper)(language);
    let symbols, edgeInfos, imports, fileDocComment, importSummary, lastLine;
    try {
        symbols = mapper.extractSymbols(rootNode);
        edgeInfos = mapper.extractEdges(rootNode);
        imports = mapper.extractImports(rootNode);
        fileDocComment = extractFileDocComment(rootNode);
        importSummary = buildImportSummary(rootNode);
        lastLine = (rootNode.endPosition?.row ?? 0) + 1;
    }
    finally {
        tree.delete();
    }
    const nodes = [];
    const edges = [];
    const fileNodeId = fileId;
    nodes.push({
        id: fileNodeId,
        attrs: makeFileAttrs(fileId, fileDocComment, importSummary, lastLine, mtime),
    });
    // --- Symbols ---
    for (const sym of symbols) {
        if (!sym.name)
            continue;
        const symbolId = makeId(fileId, sym.name);
        nodes.push({
            id: symbolId,
            attrs: {
                kind: sym.kind,
                fileId,
                name: sym.name,
                signature: sym.signature,
                docComment: sym.docComment,
                body: sym.body,
                startLine: sym.startLine,
                endLine: sym.endLine,
                isExported: sym.isExported,
                embedding: [],
                fileEmbedding: [],
                mtime,
            },
        });
        edges.push({ from: fileNodeId, to: symbolId, attrs: { kind: 'contains' } });
        // Child symbols (e.g. methods)
        if (sym.children) {
            for (const child of sym.children) {
                if (!child.name)
                    continue;
                const childId = makeId(fileId, sym.name, child.name);
                nodes.push({
                    id: childId,
                    attrs: {
                        kind: child.kind,
                        fileId,
                        name: child.name,
                        signature: child.signature,
                        docComment: child.docComment,
                        body: child.body,
                        startLine: child.startLine,
                        endLine: child.endLine,
                        isExported: child.isExported,
                        embedding: [],
                        fileEmbedding: [],
                        mtime,
                    },
                });
                edges.push({ from: symbolId, to: childId, attrs: { kind: 'contains' } });
            }
        }
    }
    // --- Extends / implements edges ---
    for (const edge of edgeInfos) {
        const fromId = makeId(fileId, edge.fromName);
        const toId = makeId(fileId, edge.toName);
        edges.push({ from: fromId, to: toId, attrs: { kind: edge.kind } });
    }
    // --- Import edges: file → imported file ---
    for (const imp of imports) {
        let targetAbsolute = null;
        if (imp.specifier.startsWith('./') || imp.specifier.startsWith('../')) {
            // Relative import
            targetAbsolute = resolveRelativeImport(absolutePath, imp.specifier);
        }
        else {
            // Try path alias resolution (e.g. @/lib/foo, ~/utils)
            targetAbsolute = resolveAliasImport(imp.specifier, absolutePath, codeDir);
        }
        if (!targetAbsolute)
            continue;
        const targetFileId = path_1.default.relative(codeDir, targetAbsolute);
        if (targetFileId.startsWith('..') || path_1.default.isAbsolute(targetFileId))
            continue;
        if (targetFileId !== fileNodeId) {
            edges.push({
                from: fileNodeId,
                to: targetFileId,
                attrs: { kind: 'imports' },
            });
        }
    }
    return { fileId, mtime, nodes, edges };
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function makeId(fileId, ...parts) {
    return [fileId, ...parts].join('::');
}
function makeFileAttrs(fileId, docComment, importSummary, lastLine, mtime) {
    return {
        kind: 'file',
        fileId,
        name: path_1.default.basename(fileId),
        signature: fileId,
        docComment,
        body: importSummary,
        startLine: 1,
        endLine: lastLine,
        isExported: false,
        embedding: [],
        fileEmbedding: [],
        mtime,
    };
}
/**
 * Extract the file-level doc comment (first JSDoc comment before any declaration).
 */
function extractFileDocComment(rootNode) {
    for (const child of rootNode.children) {
        if (child.type === 'comment' && child.text.startsWith('/**')) {
            return child.text.trim();
        }
        // Stop at first non-comment node
        if (child.type !== 'comment')
            break;
    }
    return '';
}
/**
 * Build a summary of import statements.
 */
function buildImportSummary(rootNode) {
    const imports = [];
    for (const child of rootNode.children) {
        if (child.type === 'import_statement') {
            imports.push(child.text.trim());
        }
    }
    return imports.join('\n');
}
