import { resolve, relative, join } from 'node:path';
import { stat as statAsync } from 'node:fs/promises';
import { statSync, lstatSync, existsSync, readFileSync, realpathSync, readdirSync } from 'node:fs';
import { walkDir, WALK_SKIP_DIRS as SKIP_DIRS } from '../../utils/walk-dir.js';
import { summarizeError } from '../../utils/error-display.js';
import { logger } from '../../utils/logger.js';
async function statCachedSearchFile(filePath) {
    try {
        return await statAsync(filePath);
    }
    catch {
        return null;
    }
}
const MAX_FIND_WARNINGS = 20;
export function createFindDiagnostics() {
    return { warnings: [] };
}
export function addFindWarning(diagnostics, warning) {
    if (!diagnostics)
        return;
    if (diagnostics.warnings.includes(warning))
        return;
    if (diagnostics.warnings.length < MAX_FIND_WARNINGS) {
        diagnostics.warnings.push(warning);
        return;
    }
    const cappedWarning = `Additional find warnings suppressed after ${MAX_FIND_WARNINGS} entries.`;
    if (!diagnostics.warnings.includes(cappedWarning))
        diagnostics.warnings.push(cappedWarning);
}
export function withFindWarnings(result, warnings) {
    if (warnings.length === 0)
        return result;
    const existingWarnings = Array.isArray(result.warnings)
        ? result.warnings.filter((warning) => typeof warning === 'string')
        : [];
    return {
        ...result,
        warnings: [...existingWarnings, ...warnings],
    };
}
export function makeCountResult(count, source, fileCount) {
    return fileCount !== undefined
        ? { count, file_count: fileCount, ...(source ? { source } : {}) }
        : { count, ...(source ? { source } : {}) };
}
export function makeFilesResult(files, count, source) {
    return { files, count, ...(source ? { source } : {}) };
}
export function makeLocationsResult(locations, count, source) {
    return { locations, count, ...(source ? { source } : {}) };
}
export const VALID_SYMBOL_KINDS = new Set(['function', 'class', 'interface', 'type', 'variable', 'constant', 'enum']);
const BINARY_CHECK_BYTES = 8192;
export function isHiddenOrSkippedSegment(segment, includeHidden) {
    return SKIP_DIRS.has(segment) || (!includeHidden && segment.startsWith('.') && segment !== '.');
}
export function shouldSkipRelativePath(relativePath, includeHidden) {
    return relativePath.split('/').some((segment) => isHiddenOrSkippedSegment(segment, includeHidden));
}
export async function isBinary(filePath, diagnostics) {
    try {
        const file = Bun.file(filePath);
        const size = file.size;
        if (size === 0)
            return false;
        const chunk = await file.slice(0, BINARY_CHECK_BYTES).arrayBuffer();
        const bytes = new Uint8Array(chunk);
        for (const byte of bytes) {
            if (byte === 0)
                return true;
        }
        return false;
    }
    catch (err) {
        addFindWarning(diagnostics, `Skipped '${filePath}' because binary detection failed: ${summarizeError(err)}`);
        return true;
    }
}
export async function collectTextFiles(dirPath, diagnostics) {
    const files = [];
    for await (const filePath of walkDir(dirPath)) {
        if (!(await isBinary(filePath, diagnostics))) {
            files.push(filePath);
        }
    }
    return files;
}
export async function readTextFile(filePath, diagnostics) {
    try {
        return await Bun.file(filePath).text();
    }
    catch (err) {
        addFindWarning(diagnostics, `Skipped unreadable file '${filePath}': ${summarizeError(err)}`);
        return null;
    }
}
export async function collectGlobFiles(basePath, patterns, includeHidden, followSymlinks, diagnostics) {
    const matchedFiles = new Set();
    const visitedRealPaths = new Set();
    for (const pattern of patterns) {
        let glob;
        try {
            glob = new Bun.Glob(pattern);
        }
        catch (err) {
            addFindWarning(diagnostics, `Skipped invalid glob pattern '${pattern}': ${summarizeError(err)}`);
            continue;
        }
        try {
            for await (const file of glob.scan({ cwd: basePath, onlyFiles: true, absolute: true, followSymlinks })) {
                if (followSymlinks) {
                    try {
                        const real = realpathSync(file);
                        if (visitedRealPaths.has(real))
                            continue;
                        visitedRealPaths.add(real);
                    }
                    catch (err) {
                        addFindWarning(diagnostics, `Skipped '${file}' because symlink resolution failed: ${summarizeError(err)}`);
                        continue;
                    }
                }
                const rel = relative(basePath, file);
                if (shouldSkipRelativePath(rel, includeHidden))
                    continue;
                matchedFiles.add(file);
            }
        }
        catch (err) {
            addFindWarning(diagnostics, `Glob scan failed for pattern '${pattern}': ${summarizeError(err)}`);
        }
    }
    return matchedFiles;
}
export function matchesGlob(glob, filePath, basePath) {
    const rel = relative(basePath, filePath);
    return glob.match(rel) || glob.match(filePath);
}
export function toSymbolKind(kind) {
    const kindMap = {
        method: 'function',
        property: 'variable',
        namespace: 'variable',
    };
    const mappedKind = (kindMap[kind ?? ''] ?? kind ?? 'variable');
    return VALID_SYMBOL_KINDS.has(mappedKind) ? mappedKind : 'variable';
}
export function matchesSymbolQuery(name, queryRegex) {
    return queryRegex ? queryRegex.test(name) : true;
}
export async function loadFileLines(filePath) {
    const raw = await readTextFile(filePath);
    return raw === null ? [] : raw.split('\n');
}
export function groupByKey(items, groupBy) {
    if (groupBy === 'none')
        return null;
    const grouped = {};
    for (const item of items) {
        const key = groupBy === 'file' ? item.file : item.kind;
        if (!grouped[key])
            grouped[key] = [];
        grouped[key].push(item);
    }
    return grouped;
}
export function validateSearchPath(path, projectRoot) {
    const resolved = path ? resolve(projectRoot, path) : projectRoot;
    if (!resolved.startsWith(projectRoot + '/') && resolved !== projectRoot) {
        return { error: `Path '${path}' resolves outside the project root.` };
    }
    return resolved;
}
export function buildGitignoreMatcher(gitignorePath, diagnostics) {
    if (!existsSync(gitignorePath))
        return null;
    let raw;
    try {
        raw = readFileSync(gitignorePath, 'utf8');
    }
    catch (err) {
        addFindWarning(diagnostics, `Could not read root .gitignore '${gitignorePath}': ${summarizeError(err)}`);
        return null;
    }
    const rules = [];
    for (const rawLine of raw.split('\n')) {
        const line = rawLine.trim();
        if (!line || line.startsWith('#'))
            continue;
        let negate = false;
        let pat = line;
        if (pat.startsWith('!')) {
            negate = true;
            pat = pat.slice(1);
        }
        if (pat.endsWith('/'))
            pat = pat.slice(0, -1);
        if (!pat.includes('/'))
            pat = `**/${pat}`;
        else if (pat.startsWith('/'))
            pat = pat.slice(1);
        try {
            rules.push({ negate, glob: new Bun.Glob(pat) });
        }
        catch (err) {
            addFindWarning(diagnostics, `Skipped malformed .gitignore pattern '${line}': ${summarizeError(err)}`);
        }
    }
    if (rules.length === 0)
        return null;
    return (rel) => {
        let ignored = false;
        for (const rule of rules) {
            if (rule.glob.match(rel)) {
                ignored = !rule.negate;
            }
        }
        return ignored;
    };
}
export function findNestedGitignoreFiles(basePath, rootGitignorePath) {
    const nested = [];
    const maxNested = 5;
    const visit = (dir) => {
        if (nested.length >= maxNested)
            return;
        let entries;
        try {
            entries = readdirSync(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            if (nested.length >= maxNested)
                return;
            if (SKIP_DIRS.has(entry.name))
                continue;
            const full = join(dir, entry.name);
            if (entry.isDirectory()) {
                visit(full);
            }
            else if (entry.isFile() && entry.name === '.gitignore' && full !== rootGitignorePath) {
                nested.push(full);
            }
        }
    };
    visit(basePath);
    return nested;
}
export async function collectFilesForSearch(basePath, queryGlob, diagnostics) {
    if (!queryGlob) {
        return collectTextFiles(basePath, diagnostics);
    }
    return Array.from(await collectGlobFiles(basePath, [queryGlob], false, false, diagnostics));
}
const SEARCH_CACHE_MAX = 50;
const IMPORT_GRAPH_TTL = 30_000;
function makeSearchCacheKey(key) {
    return JSON.stringify([key.pattern, key.glob, key.path, key.flags]);
}
export class FindRuntimeService {
    searchCache = new Map();
    importGraph = null;
    importGraphBuiltAt = 0;
    searchCacheGet(key) {
        const cacheKey = makeSearchCacheKey(key);
        const entry = this.searchCache.get(cacheKey);
        if (!entry)
            return null;
        entry.accessedAt = Date.now();
        return entry.value;
    }
    searchCacheSet(key, value) {
        const cacheKey = makeSearchCacheKey(key);
        if (this.searchCache.size >= SEARCH_CACHE_MAX && !this.searchCache.has(cacheKey)) {
            let oldestKey = '';
            let oldestTime = Infinity;
            for (const [candidateKey, candidateValue] of this.searchCache) {
                if (candidateValue.accessedAt < oldestTime) {
                    oldestTime = candidateValue.accessedAt;
                    oldestKey = candidateKey;
                }
            }
            if (oldestKey)
                this.searchCache.delete(oldestKey);
        }
        this.searchCache.set(cacheKey, { value, accessedAt: Date.now() });
    }
    async searchCacheIsValid(cached) {
        const entries = Array.from(cached.fileMtimes.entries());
        const stats = await Promise.all(entries.map(([filePath]) => statCachedSearchFile(filePath)));
        for (let i = 0; i < entries.length; i++) {
            const stat = stats[i];
            if (!stat || stat.mtimeMs !== entries[i][1])
                return false;
        }
        return true;
    }
    async getImportGraph(projectRoot) {
        const now = Date.now();
        if (this.importGraph !== null && now - this.importGraphBuiltAt <= IMPORT_GRAPH_TTL) {
            return this.importGraph;
        }
        const { ImportGraph } = await import('../../intelligence/import-graph.js');
        const graph = new ImportGraph();
        try {
            await graph.build(projectRoot);
        }
        catch (err) {
            const warning = `Import graph build failed; relationship results may be incomplete: ${summarizeError(err)}`;
            logger.warn('[find] Import graph build failed', { projectRoot, error: summarizeError(err) });
            const degradedGraph = {
                findImports: (file) => graph.findImports(file),
                findDependents: (file) => graph.findDependents(file),
                getWarnings: () => [
                    warning,
                    ...(graph.getWarnings?.() ?? []),
                ],
            };
            this.importGraph = degradedGraph;
            this.importGraphBuiltAt = now;
            return degradedGraph;
        }
        this.importGraph = graph;
        this.importGraphBuiltAt = now;
        return graph;
    }
}
