/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs/promises';
import * as fsSync from 'node:fs';
import * as path from 'node:path';
import { homedir } from 'node:os';
import { bfsFileSearch } from './bfsFileSearch.js';
import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
import { processImports } from './memoryImportProcessor.js';
import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/config.js';
import { GEMINI_DIR } from './paths.js';
// Simple console logger, similar to the one previously in CLI's config.ts
// TODO: Integrate with a more robust server-side logger if available/appropriate.
const logger = {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    debug: (...args) => console.debug('[DEBUG] [MemoryDiscovery]', ...args),
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    warn: (...args) => console.warn('[WARN] [MemoryDiscovery]', ...args),
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    error: (...args) => console.error('[ERROR] [MemoryDiscovery]', ...args),
};
async function findProjectRoot(startDir) {
    let currentDir = path.resolve(startDir);
    while (true) {
        const gitPath = path.join(currentDir, '.git');
        try {
            const stats = await fs.lstat(gitPath);
            if (stats.isDirectory()) {
                return currentDir;
            }
        }
        catch (error) {
            // Don't log ENOENT errors as they're expected when .git doesn't exist
            // Also don't log errors in test environments, which often have mocked fs
            const isENOENT = typeof error === 'object' &&
                error !== null &&
                'code' in error &&
                error.code === 'ENOENT';
            // Only log unexpected errors in non-test environments
            // process.env['NODE_ENV'] === 'test' or VITEST are common test indicators
            const isTestEnv = process.env['NODE_ENV'] === 'test' || process.env['VITEST'];
            if (!isENOENT && !isTestEnv) {
                if (typeof error === 'object' && error !== null && 'code' in error) {
                    const fsError = error;
                    logger.warn(`Error checking for .git directory at ${gitPath}: ${fsError.message}`);
                }
                else {
                    logger.warn(`Non-standard error checking for .git directory at ${gitPath}: ${String(error)}`);
                }
            }
        }
        const parentDir = path.dirname(currentDir);
        if (parentDir === currentDir) {
            return null;
        }
        currentDir = parentDir;
    }
}
async function getGeminiMdFilePathsInternal(currentWorkingDirectory, includeDirectoriesToReadGemini, userHomePath, debugMode, fileService, extensionContextFilePaths = [], folderTrust, fileFilteringOptions, maxDirs) {
    const dirs = new Set([
        ...includeDirectoriesToReadGemini,
        currentWorkingDirectory,
    ]);
    // Process directories in parallel with concurrency limit to prevent EMFILE errors
    const CONCURRENT_LIMIT = 10;
    const dirsArray = Array.from(dirs);
    const pathsArrays = [];
    for (let i = 0; i < dirsArray.length; i += CONCURRENT_LIMIT) {
        const batch = dirsArray.slice(i, i + CONCURRENT_LIMIT);
        const batchPromises = batch.map((dir) => getGeminiMdFilePathsInternalForEachDir(dir, userHomePath, debugMode, fileService, extensionContextFilePaths, folderTrust, fileFilteringOptions, maxDirs));
        const batchResults = await Promise.allSettled(batchPromises);
        for (const result of batchResults) {
            if (result.status === 'fulfilled') {
                pathsArrays.push(result.value);
            }
            else {
                const error = result.reason;
                const message = error instanceof Error ? error.message : String(error);
                logger.error(`Error discovering files in directory: ${message}`);
                // Continue processing other directories
            }
        }
    }
    const paths = pathsArrays.flat();
    return Array.from(new Set(paths));
}
async function getGeminiMdFilePathsInternalForEachDir(dir, userHomePath, debugMode, fileService, extensionContextFilePaths = [], folderTrust, fileFilteringOptions, maxDirs) {
    const allPaths = new Set();
    const geminiMdFilenames = getAllGeminiMdFilenames();
    for (const geminiMdFilename of geminiMdFilenames) {
        const resolvedHome = path.resolve(userHomePath);
        const globalMemoryPath = path.join(resolvedHome, GEMINI_DIR, geminiMdFilename);
        // This part that finds the global file always runs.
        try {
            await fs.access(globalMemoryPath, fsSync.constants.R_OK);
            allPaths.add(globalMemoryPath);
            if (debugMode)
                logger.debug(`Found readable global ${geminiMdFilename}: ${globalMemoryPath}`);
        }
        catch {
            // It's okay if it's not found.
        }
        // FIX: Only perform the workspace search (upward and downward scans)
        // if a valid currentWorkingDirectory is provided.
        if (dir && folderTrust) {
            const resolvedCwd = path.resolve(dir);
            if (debugMode)
                logger.debug(`Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`);
            const projectRoot = await findProjectRoot(resolvedCwd);
            if (debugMode)
                logger.debug(`Determined project root: ${projectRoot ?? 'None'}`);
            const upwardPaths = [];
            let currentDir = resolvedCwd;
            const ultimateStopDir = projectRoot
                ? path.dirname(projectRoot)
                : path.dirname(resolvedHome);
            while (currentDir && currentDir !== path.dirname(currentDir)) {
                if (currentDir === path.join(resolvedHome, GEMINI_DIR)) {
                    break;
                }
                const potentialPath = path.join(currentDir, geminiMdFilename);
                try {
                    await fs.access(potentialPath, fsSync.constants.R_OK);
                    if (potentialPath !== globalMemoryPath) {
                        upwardPaths.unshift(potentialPath);
                    }
                }
                catch {
                    // Not found, continue.
                }
                if (currentDir === ultimateStopDir) {
                    break;
                }
                currentDir = path.dirname(currentDir);
            }
            upwardPaths.forEach((p) => allPaths.add(p));
            const mergedOptions = {
                ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
                ...fileFilteringOptions,
            };
            const downwardPaths = await bfsFileSearch(resolvedCwd, {
                fileName: geminiMdFilename,
                maxDirs,
                debug: debugMode,
                fileService,
                fileFilteringOptions: mergedOptions,
            });
            downwardPaths.sort();
            for (const dPath of downwardPaths) {
                allPaths.add(dPath);
            }
        }
    }
    // Add extension context file paths.
    for (const extensionPath of extensionContextFilePaths) {
        allPaths.add(extensionPath);
    }
    const finalPaths = Array.from(allPaths);
    if (debugMode)
        logger.debug(`Final ordered ${getAllGeminiMdFilenames()} paths to read: ${JSON.stringify(finalPaths)}`);
    return finalPaths;
}
async function readGeminiMdFiles(filePaths, debugMode, importFormat = 'tree') {
    // Process files in parallel with concurrency limit to prevent EMFILE errors
    const CONCURRENT_LIMIT = 20; // Higher limit for file reads as they're typically faster
    const results = [];
    for (let i = 0; i < filePaths.length; i += CONCURRENT_LIMIT) {
        const batch = filePaths.slice(i, i + CONCURRENT_LIMIT);
        const batchPromises = batch.map(async (filePath) => {
            try {
                const content = await fs.readFile(filePath, 'utf-8');
                // Process imports in the content
                const processedResult = await processImports(content, path.dirname(filePath), debugMode, undefined, undefined, importFormat);
                if (debugMode)
                    logger.debug(`Successfully read and processed imports: ${filePath} (Length: ${processedResult.content.length})`);
                return { filePath, content: processedResult.content };
            }
            catch (error) {
                const isTestEnv = process.env['NODE_ENV'] === 'test' || process.env['VITEST'];
                if (!isTestEnv) {
                    const message = error instanceof Error ? error.message : String(error);
                    logger.warn(`Warning: Could not read ${getAllGeminiMdFilenames()} file at ${filePath}. Error: ${message}`);
                }
                if (debugMode)
                    logger.debug(`Failed to read: ${filePath}`);
                return { filePath, content: null }; // Still include it with null content
            }
        });
        const batchResults = await Promise.allSettled(batchPromises);
        for (const result of batchResults) {
            if (result.status === 'fulfilled') {
                results.push(result.value);
            }
            else {
                // This case shouldn't happen since we catch all errors above,
                // but handle it for completeness
                const error = result.reason;
                const message = error instanceof Error ? error.message : String(error);
                logger.error(`Unexpected error processing file: ${message}`);
            }
        }
    }
    return results;
}
function concatenateInstructions(instructionContents, 
// CWD is needed to resolve relative paths for display markers
currentWorkingDirectoryForDisplay) {
    return instructionContents
        .filter((item) => typeof item.content === 'string')
        .map((item) => {
        const trimmedContent = item.content.trim();
        if (trimmedContent.length === 0) {
            return null;
        }
        const displayPath = path.isAbsolute(item.filePath)
            ? path.relative(currentWorkingDirectoryForDisplay, item.filePath)
            : item.filePath;
        return `--- Context from: ${displayPath} ---\n${trimmedContent}\n--- End of Context from: ${displayPath} ---`;
    })
        .filter((block) => block !== null)
        .join('\n\n');
}
/**
 * Loads hierarchical GEMINI.md files and concatenates their content.
 * This function is intended for use by the server.
 */
export async function loadServerHierarchicalMemory(currentWorkingDirectory, includeDirectoriesToReadGemini, debugMode, fileService, extensionContextFilePaths = [], folderTrust, importFormat = 'tree', fileFilteringOptions, maxDirs = 200) {
    if (debugMode)
        logger.debug(`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`);
    // For the server, homedir() refers to the server process's home.
    // This is consistent with how MemoryTool already finds the global path.
    const userHomePath = homedir();
    const filePaths = await getGeminiMdFilePathsInternal(currentWorkingDirectory, includeDirectoriesToReadGemini, userHomePath, debugMode, fileService, extensionContextFilePaths, folderTrust, fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, maxDirs);
    if (filePaths.length === 0) {
        if (debugMode)
            logger.debug('No GEMINI.md files found in hierarchy of the workspace.');
        return { memoryContent: '', fileCount: 0 };
    }
    const contentsWithPaths = await readGeminiMdFiles(filePaths, debugMode, importFormat);
    // Pass CWD for relative path display in concatenated content
    const combinedInstructions = concatenateInstructions(contentsWithPaths, currentWorkingDirectory);
    if (debugMode)
        logger.debug(`Combined instructions length: ${combinedInstructions.length}`);
    if (debugMode && combinedInstructions.length > 0)
        logger.debug(`Combined instructions (snippet): ${combinedInstructions.substring(0, 500)}...`);
    return {
        memoryContent: combinedInstructions,
        fileCount: contentsWithPaths.length,
    };
}
//# sourceMappingURL=memoryDiscovery.js.map