/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import path from 'node:path';
import os from 'node:os';
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
export const GEMINI_DIR = '.gemini';
export const GOOGLE_ACCOUNTS_FILENAME = 'google_accounts.json';
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
/**
 * Returns the home directory.
 * If GEMINI_CLI_HOME environment variable is set, it returns its value.
 * Otherwise, it returns the user's home directory.
 */
export function homedir() {
    const envHome = process.env['GEMINI_CLI_HOME'];
    if (envHome) {
        return envHome;
    }
    return os.homedir();
}
/**
 * Returns the operating system's default directory for temporary files.
 */
export function tmpdir() {
    return os.tmpdir();
}
/**
 * Replaces the home directory with a tilde.
 * @param path - The path to tildeify.
 * @returns The tildeified path.
 */
export function tildeifyPath(path) {
    const homeDir = homedir();
    if (path.startsWith(homeDir)) {
        return path.replace(homeDir, '~');
    }
    return path;
}
/**
 * Shortens a path string if it exceeds maxLen, prioritizing the start and end segments.
 * Example: /path/to/a/very/long/file.txt -> /path/.../long/file.txt
 */
export function shortenPath(filePath, maxLen = 35) {
    if (filePath.length <= maxLen) {
        return filePath;
    }
    const simpleTruncate = () => {
        const keepLen = Math.floor((maxLen - 3) / 2);
        if (keepLen <= 0) {
            return filePath.substring(0, maxLen - 3) + '...';
        }
        const start = filePath.substring(0, keepLen);
        const end = filePath.substring(filePath.length - keepLen);
        return `${start}...${end}`;
    };
    const truncateComponent = (component, targetLength, mode) => {
        if (component.length <= targetLength) {
            return component;
        }
        if (targetLength <= 0) {
            return '';
        }
        if (targetLength <= 3) {
            if (mode === 'end') {
                return component.slice(-targetLength);
            }
            return component.slice(0, targetLength);
        }
        if (mode === 'start') {
            return `${component.slice(0, targetLength - 3)}...`;
        }
        if (mode === 'end') {
            return `...${component.slice(component.length - (targetLength - 3))}`;
        }
        const front = Math.ceil((targetLength - 3) / 2);
        const back = targetLength - 3 - front;
        return `${component.slice(0, front)}...${component.slice(component.length - back)}`;
    };
    const parsedPath = path.parse(filePath);
    const root = parsedPath.root;
    const separator = path.sep;
    // Get segments of the path *after* the root
    const relativePath = filePath.substring(root.length);
    const segments = relativePath.split(separator).filter((s) => s !== ''); // Filter out empty segments
    // Handle cases with no segments after root (e.g., "/", "C:\") or only one segment
    if (segments.length <= 1) {
        // Fall back to simple start/end truncation for very short paths or single segments
        return simpleTruncate();
    }
    const firstDir = segments[0];
    const lastSegment = segments[segments.length - 1];
    const startComponent = root + firstDir;
    const endPartSegments = [lastSegment];
    let endPartLength = lastSegment.length;
    // Iterate backwards through the middle segments
    for (let i = segments.length - 2; i > 0; i--) {
        const segment = segments[i];
        const newLength = startComponent.length +
            separator.length +
            3 + // for "..."
            separator.length +
            endPartLength +
            separator.length +
            segment.length;
        if (newLength <= maxLen) {
            endPartSegments.unshift(segment);
            endPartLength += separator.length + segment.length;
        }
        else {
            break;
        }
    }
    const components = [firstDir, ...endPartSegments];
    const componentModes = components.map((_, index) => {
        if (index === 0) {
            return 'start';
        }
        if (index === components.length - 1) {
            return 'end';
        }
        return 'center';
    });
    const separatorsCount = endPartSegments.length + 1;
    const fixedLen = root.length + separatorsCount * separator.length + 3; // ellipsis length
    const availableForComponents = maxLen - fixedLen;
    const trailingFallback = () => {
        const ellipsisTail = `...${separator}${lastSegment}`;
        if (ellipsisTail.length <= maxLen) {
            return ellipsisTail;
        }
        if (root) {
            const rootEllipsisTail = `${root}...${separator}${lastSegment}`;
            if (rootEllipsisTail.length <= maxLen) {
                return rootEllipsisTail;
            }
        }
        if (root && `${root}${lastSegment}`.length <= maxLen) {
            return `${root}${lastSegment}`;
        }
        if (lastSegment.length <= maxLen) {
            return lastSegment;
        }
        // As a final resort (e.g., last segment itself exceeds maxLen), fall back to simple truncation.
        return simpleTruncate();
    };
    if (availableForComponents <= 0) {
        return trailingFallback();
    }
    const minLengths = components.map((component, index) => {
        if (index === 0) {
            return Math.min(component.length, 1);
        }
        if (index === components.length - 1) {
            return component.length; // Never truncate the last segment when possible.
        }
        return Math.min(component.length, 1);
    });
    const minTotal = minLengths.reduce((sum, len) => sum + len, 0);
    if (availableForComponents < minTotal) {
        return trailingFallback();
    }
    const budgets = components.map((component) => component.length);
    let currentTotal = budgets.reduce((sum, len) => sum + len, 0);
    const pickIndexToReduce = () => {
        let bestIndex = -1;
        let bestScore = -Infinity;
        for (let i = 0; i < budgets.length; i++) {
            if (budgets[i] <= minLengths[i]) {
                continue;
            }
            const isLast = i === budgets.length - 1;
            const score = (isLast ? 0 : 1_000_000) + budgets[i];
            if (score > bestScore) {
                bestScore = score;
                bestIndex = i;
            }
        }
        return bestIndex;
    };
    while (currentTotal > availableForComponents) {
        const index = pickIndexToReduce();
        if (index === -1) {
            return trailingFallback();
        }
        budgets[index]--;
        currentTotal--;
    }
    const truncatedComponents = components.map((component, index) => truncateComponent(component, budgets[index], componentModes[index]));
    const truncatedFirst = truncatedComponents[0];
    const truncatedEnd = truncatedComponents.slice(1).join(separator);
    const result = `${root}${truncatedFirst}${separator}...${separator}${truncatedEnd}`;
    if (result.length > maxLen) {
        return trailingFallback();
    }
    return result;
}
/**
 * Calculates the relative path from a root directory to a target path.
 * If targetPath is relative, it is returned as-is.
 * Returns '.' if the target path is the same as the root directory.
 *
 * @param targetPath The absolute or relative path to make relative.
 * @param rootDirectory The absolute path of the directory to make the target path relative to.
 * @returns The relative path from rootDirectory to targetPath.
 */
export function makeRelative(targetPath, rootDirectory) {
    if (!path.isAbsolute(targetPath)) {
        return targetPath;
    }
    const resolvedRootDirectory = path.resolve(rootDirectory);
    const relativePath = path.relative(resolvedRootDirectory, targetPath);
    // If the paths are the same, path.relative returns '', return '.' instead
    return relativePath || '.';
}
/**
 * Escape paths for at-commands.
 *
 *  - Windows: double quoted if they contain special chars, otherwise bare
 *  - POSIX: backslash-escaped
 */
export function escapePath(filePath) {
    if (process.platform === 'win32') {
        // Windows: Double quote if it contains special chars
        if (/[\s&()[\]{}^=;!'+,`~%$@#]/.test(filePath)) {
            return `"${filePath}"`;
        }
        return filePath;
    }
    else {
        // POSIX: Backslash escape
        return filePath.replace(/([ \t()[\]{};|*?$`'"#&<>!~\\])/g, '\\$1');
    }
}
/**
 * Unescapes paths for at-commands.
 *
 *  - Windows: double quoted if they contain special chars, otherwise bare
 *  - POSIX: backslash-escaped
 */
export function unescapePath(filePath) {
    if (process.platform === 'win32') {
        if (filePath.length >= 2 &&
            filePath.startsWith('"') &&
            filePath.endsWith('"')) {
            return filePath.slice(1, -1);
        }
        return filePath;
    }
    else {
        return filePath.replace(/\\(.)/g, '$1');
    }
}
/**
 * Generates a unique hash for a project based on its root path.
 * @param projectRoot The absolute path to the project's root directory.
 * @returns A SHA256 hash of the project root path.
 */
export function getProjectHash(projectRoot) {
    return crypto.createHash('sha256').update(projectRoot).digest('hex');
}
/**
 * Normalizes a path for reliable comparison across platforms.
 * - Resolves to an absolute path.
 * - Converts all path separators to forward slashes.
 * - On Windows, converts to lowercase for case-insensitivity.
 */
export function normalizePath(p) {
    const platform = process.platform;
    const isWindows = platform === 'win32';
    const pathModule = isWindows ? path.win32 : path;
    const resolved = pathModule.resolve(p);
    const normalized = resolved.replace(/\\/g, '/');
    const isCaseInsensitive = isWindows || platform === 'darwin';
    return isCaseInsensitive ? normalized.toLowerCase() : normalized;
}
/**
 * Checks if a path is a subpath of another path.
 * @param parentPath The parent path.
 * @param childPath The child path.
 * @returns True if childPath is a subpath of parentPath, false otherwise.
 */
export function isSubpath(parentPath, childPath) {
    const platform = process.platform;
    const isWindows = platform === 'win32';
    const isDarwin = platform === 'darwin';
    const pathModule = isWindows ? path.win32 : path;
    // Resolve both paths to absolute to ensure consistent comparison,
    // especially when mixing relative and absolute paths or when casing differs.
    let p = pathModule.resolve(parentPath);
    let c = pathModule.resolve(childPath);
    // On Windows, path.relative is case-insensitive.
    // On POSIX (including Darwin), path.relative is case-sensitive.
    // We want it to be case-insensitive on Darwin to match user expectation and sandbox policy.
    if (isDarwin) {
        p = p.toLowerCase();
        c = c.toLowerCase();
    }
    const relative = pathModule.relative(p, c);
    return (!relative.startsWith(`..${pathModule.sep}`) &&
        relative !== '..' &&
        !pathModule.isAbsolute(relative));
}
/**
 * Type guard to verify a value is a string and does not contain null bytes.
 */
export function isValidPathString(p) {
    return typeof p === 'string' && !p.includes('\0');
}
/**
 * Asserts that a value is a valid path string, throwing an Error otherwise.
 */
export function assertValidPathString(p) {
    if (!isValidPathString(p)) {
        throw new Error(`Invalid path: ${String(p)}`);
    }
}
/**
 * Resolves a path to its real path, sanitizing it first.
 * - Removes 'file://' protocol if present.
 * - Decodes URI components (e.g. %20 -> space).
 * - Resolves symbolic links using fs.realpathSync.
 *
 * @param pathStr The path string to resolve.
 * @returns The resolved real path.
 */
export function resolveToRealPath(pathStr) {
    assertValidPathString(pathStr);
    let resolvedPath = pathStr;
    try {
        if (resolvedPath.startsWith('file://')) {
            resolvedPath = fileURLToPath(resolvedPath);
        }
        resolvedPath = decodeURIComponent(resolvedPath);
    }
    catch {
        // Ignore error (e.g. malformed URI), keep path from previous step
    }
    return robustRealpath(path.resolve(resolvedPath));
}
function robustRealpath(p, visited = new Set()) {
    const key = process.platform === 'win32' ? p.toLowerCase() : p;
    if (visited.has(key)) {
        throw new Error(`Infinite recursion detected in robustRealpath: ${p}`);
    }
    visited.add(key);
    try {
        return fs.realpathSync(p);
    }
    catch (e) {
        if (e &&
            typeof e === 'object' &&
            'code' in e &&
            (e.code === 'ENOENT' || e.code === 'EISDIR')) {
            try {
                const stat = fs.lstatSync(p);
                if (stat.isSymbolicLink()) {
                    const target = fs.readlinkSync(p);
                    const resolvedTarget = path.resolve(path.dirname(p), target);
                    return robustRealpath(resolvedTarget, visited);
                }
            }
            catch (lstatError) {
                // Not a symlink, or lstat failed. Re-throw if it's not an expected
                // ENOENT (e.g., a permissions error), otherwise resolve parent.
                if (!(lstatError &&
                    typeof lstatError === 'object' &&
                    'code' in lstatError &&
                    (lstatError.code === 'ENOENT' || lstatError.code === 'EISDIR'))) {
                    throw lstatError;
                }
            }
            const parent = path.dirname(p);
            if (parent === p)
                return p;
            return path.join(robustRealpath(parent, visited), path.basename(p));
        }
        throw e;
    }
}
/**
 * Deduplicates an array of paths and ensures all paths are absolute.
 */
export function deduplicateAbsolutePaths(paths) {
    if (!paths || paths.length === 0)
        return [];
    const uniquePathsMap = new Map();
    for (const p of paths) {
        if (!path.isAbsolute(p)) {
            throw new Error(`Path must be absolute: ${p}`);
        }
        const key = toPathKey(p);
        if (!uniquePathsMap.has(key)) {
            uniquePathsMap.set(key, p);
        }
    }
    return Array.from(uniquePathsMap.values());
}
/**
 * Returns a stable string key for a path to be used in comparisons or Map lookups.
 */
export function toPathKey(p) {
    // Normalize path segments
    let norm = path.normalize(p);
    // Strip trailing slashes (except for root paths)
    if (norm.length > 1 && (norm.endsWith('/') || norm.endsWith('\\'))) {
        // On Windows, don't strip the slash from a drive root (e.g., "C:\\")
        if (!/^[a-zA-Z]:[\\/]$/.test(norm)) {
            norm = norm.slice(0, -1);
        }
    }
    // Convert to lowercase on case-insensitive platforms
    const platform = process.platform;
    const isCaseInsensitive = platform === 'win32' || platform === 'darwin';
    return isCaseInsensitive ? norm.toLowerCase() : norm;
}
//# sourceMappingURL=paths.js.map