/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { getErrorMessage, isNodeError } from './errors.js';
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js';
import { debugLogger } from './debugLogger.js';
const MAX_ITEMS = 200;
const TRUNCATION_INDICATOR = '...';
const DEFAULT_IGNORED_FOLDERS = new Set(['node_modules', '.git', 'dist']);
// --- Interfaces ---
// --- Helper Functions ---
async function readFullStructure(rootPath, options) {
    const rootName = path.basename(rootPath);
    const rootNode = {
        name: rootName,
        path: rootPath,
        files: [],
        subFolders: [],
        totalChildren: 0,
        totalFiles: 0,
    };
    const queue = [
        { folderInfo: rootNode, currentPath: rootPath },
    ];
    let currentItemCount = 0;
    // Count the root node itself as one item if we are not just listing its content
    const processedPaths = new Set(); // To avoid processing same path if symlinks create loops
    while (queue.length > 0) {
        const { folderInfo, currentPath } = queue.shift();
        if (processedPaths.has(currentPath)) {
            continue;
        }
        processedPaths.add(currentPath);
        if (currentItemCount >= options.maxItems) {
            // If the root itself caused us to exceed, we can't really show anything.
            // Otherwise, this folder won't be processed further.
            // The parent that queued this would have set its own hasMoreSubfolders flag.
            continue;
        }
        let entries;
        try {
            const rawEntries = await fs.readdir(currentPath, { withFileTypes: true });
            // Sort entries alphabetically by name for consistent processing order
            entries = rawEntries.sort((a, b) => a.name.localeCompare(b.name));
        }
        catch (error) {
            if (isNodeError(error) &&
                (error.code === 'EACCES' || error.code === 'ENOENT')) {
                debugLogger.warn(`Warning: Could not read directory ${currentPath}: ${error.message}`);
                if (currentPath === rootPath && error.code === 'ENOENT') {
                    return null; // Root directory itself not found
                }
                // For other EACCES/ENOENT on subdirectories, just skip them.
                continue;
            }
            throw error;
        }
        const filesInCurrentDir = [];
        const subFoldersInCurrentDir = [];
        const filterFileOptions = {
            respectGitIgnore: options.fileFilteringOptions?.respectGitIgnore,
            respectGeminiIgnore: options.fileFilteringOptions?.respectGeminiIgnore,
        };
        // Process files first in the current directory
        for (const entry of entries) {
            if (entry.isFile()) {
                if (currentItemCount >= options.maxItems) {
                    folderInfo.hasMoreFiles = true;
                    break;
                }
                const fileName = entry.name;
                const filePath = path.join(currentPath, fileName);
                if (options.fileService?.shouldIgnoreFile(filePath, filterFileOptions)) {
                    continue;
                }
                if (!options.fileIncludePattern ||
                    options.fileIncludePattern.test(fileName)) {
                    filesInCurrentDir.push(fileName);
                    currentItemCount++;
                    folderInfo.totalFiles++;
                    folderInfo.totalChildren++;
                }
            }
        }
        folderInfo.files = filesInCurrentDir;
        // Then process directories and queue them
        for (const entry of entries) {
            if (entry.isDirectory()) {
                // Check if adding this directory ITSELF would meet or exceed maxItems
                // (currentItemCount refers to items *already* added before this one)
                if (currentItemCount >= options.maxItems) {
                    folderInfo.hasMoreSubfolders = true;
                    break; // Already at limit, cannot add this folder or any more
                }
                // If adding THIS folder makes us hit the limit exactly, and it might have children,
                // it's better to show '...' for the parent, unless this is the very last item slot.
                // This logic is tricky. Let's try a simpler: if we can't add this item, mark and break.
                const subFolderName = entry.name;
                const subFolderPath = path.join(currentPath, subFolderName);
                const isIgnored = options.fileService?.shouldIgnoreFile(subFolderPath, filterFileOptions) ?? false;
                if (options.ignoredFolders.has(subFolderName) || isIgnored) {
                    const ignoredSubFolder = {
                        name: subFolderName,
                        path: subFolderPath,
                        files: [],
                        subFolders: [],
                        totalChildren: 0,
                        totalFiles: 0,
                        isIgnored: true,
                    };
                    subFoldersInCurrentDir.push(ignoredSubFolder);
                    currentItemCount++; // Count the ignored folder itself
                    folderInfo.totalChildren++; // Also counts towards parent's children
                    continue;
                }
                const subFolderNode = {
                    name: subFolderName,
                    path: subFolderPath,
                    files: [],
                    subFolders: [],
                    totalChildren: 0,
                    totalFiles: 0,
                };
                subFoldersInCurrentDir.push(subFolderNode);
                currentItemCount++;
                folderInfo.totalChildren++; // Counts towards parent's children
                // Add to queue for processing its children later
                queue.push({ folderInfo: subFolderNode, currentPath: subFolderPath });
            }
        }
        folderInfo.subFolders = subFoldersInCurrentDir;
    }
    return rootNode;
}
/**
 * Reads the directory structure using BFS, respecting maxItems.
 * @param node The current node in the reduced structure.
 * @param indent The current indentation string.
 * @param isLast Sibling indicator.
 * @param builder Array to build the string lines.
 */
function formatStructure(node, currentIndent, isLastChildOfParent, isProcessingRootNode, builder) {
    const connector = isLastChildOfParent ? '└───' : '├───';
    // The root node of the structure (the one passed initially to getFolderStructure)
    // is not printed with a connector line itself, only its name as a header.
    // Its children are printed relative to that conceptual root.
    // Ignored root nodes ARE printed with a connector.
    if (!isProcessingRootNode || node.isIgnored) {
        builder.push(`${currentIndent}${connector}${node.name}${path.sep}${node.isIgnored ? TRUNCATION_INDICATOR : ''}`);
    }
    // Determine the indent for the children of *this* node.
    // If *this* node was the root of the whole structure, its children start with no indent before their connectors.
    // Otherwise, children's indent extends from the current node's indent.
    const indentForChildren = isProcessingRootNode
        ? ''
        : currentIndent + (isLastChildOfParent ? '    ' : '│   ');
    // Render files of the current node
    const fileCount = node.files.length;
    for (let i = 0; i < fileCount; i++) {
        const isLastFileAmongSiblings = i === fileCount - 1 &&
            node.subFolders.length === 0 &&
            !node.hasMoreSubfolders;
        const fileConnector = isLastFileAmongSiblings ? '└───' : '├───';
        builder.push(`${indentForChildren}${fileConnector}${node.files[i]}`);
    }
    if (node.hasMoreFiles) {
        const isLastIndicatorAmongSiblings = node.subFolders.length === 0 && !node.hasMoreSubfolders;
        const fileConnector = isLastIndicatorAmongSiblings ? '└───' : '├───';
        builder.push(`${indentForChildren}${fileConnector}${TRUNCATION_INDICATOR}`);
    }
    // Render subfolders of the current node
    const subFolderCount = node.subFolders.length;
    for (let i = 0; i < subFolderCount; i++) {
        const isLastSubfolderAmongSiblings = i === subFolderCount - 1 && !node.hasMoreSubfolders;
        // Children are never the root node being processed initially.
        formatStructure(node.subFolders[i], indentForChildren, isLastSubfolderAmongSiblings, false, builder);
    }
    if (node.hasMoreSubfolders) {
        builder.push(`${indentForChildren}└───${TRUNCATION_INDICATOR}`);
    }
}
// --- Main Exported Function ---
/**
 * Generates a string representation of a directory's structure,
 * limiting the number of items displayed. Ignored folders are shown
 * followed by '...' instead of their contents.
 *
 * @param directory The absolute or relative path to the directory.
 * @param options Optional configuration settings.
 * @returns A promise resolving to the formatted folder structure string.
 */
export async function getFolderStructure(directory, options) {
    const resolvedPath = path.resolve(directory);
    const mergedOptions = {
        maxItems: options?.maxItems ?? MAX_ITEMS,
        ignoredFolders: options?.ignoredFolders ?? DEFAULT_IGNORED_FOLDERS,
        fileIncludePattern: options?.fileIncludePattern,
        fileService: options?.fileService,
        fileFilteringOptions: options?.fileFilteringOptions ?? DEFAULT_FILE_FILTERING_OPTIONS,
    };
    try {
        // 1. Read the structure using BFS, respecting maxItems
        const structureRoot = await readFullStructure(resolvedPath, mergedOptions);
        if (!structureRoot) {
            return `Error: Could not read directory "${resolvedPath}". Check path and permissions.`;
        }
        // 2. Format the structure into a string
        const structureLines = [];
        // Pass true for isRoot for the initial call
        formatStructure(structureRoot, '', true, true, structureLines);
        // 3. Build the final output string
        function isTruncated(node) {
            if (node.hasMoreFiles || node.hasMoreSubfolders || node.isIgnored) {
                return true;
            }
            for (const sub of node.subFolders) {
                if (isTruncated(sub)) {
                    return true;
                }
            }
            return false;
        }
        let summary = `Showing up to ${mergedOptions.maxItems} items (files + folders).`;
        if (isTruncated(structureRoot)) {
            summary += ` Folders or files indicated with ${TRUNCATION_INDICATOR} contain more items not shown, were ignored, or the display limit (${mergedOptions.maxItems} items) was reached.`;
        }
        return `${summary}\n\n${resolvedPath}${path.sep}\n${structureLines.join('\n')}`;
    }
    catch (error) {
        console.error(`Error getting folder structure for ${resolvedPath}:`, error);
        return `Error processing directory "${resolvedPath}": ${getErrorMessage(error)}`;
    }
}
//# sourceMappingURL=getFolderStructure.js.map