// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { createHash } from 'node:crypto';
import ignore from 'ignore';
import { LookupByPath } from '@rushstack/lookup-by-path';
import { InternalError, Path, Sort } from '@rushstack/node-core-library';
import { RushConstants } from '../RushConstants';
const { hashDelimiter } = RushConstants;
/**
 * Represents a synchronously-queryable in-memory snapshot of the state of the inputs to a Rush repository.
 * Any asynchronous work needs to be performed by the caller and the results passed to the constructor.
 *
 * @remarks
 * All operations on this class will return the same result regardless of when they are executed.
 *
 * @internal
 */
export class InputsSnapshot {
    /**
     *
     * @param params - The parameters for the snapshot
     * @internal
     */
    constructor(params) {
        const { additionalHashes, environment = { ...process.env }, globalAdditionalFiles, hashes, hasUncommittedChanges, lookupByPath, rootDir } = params;
        const projectMetadataMap = new Map();
        for (const [project, record] of params.projectMap) {
            projectMetadataMap.set(project, createInternalRecord(project, record, rootDir));
        }
        // Route hashes to individual projects
        for (const [file, hash] of hashes) {
            const project = lookupByPath.findChildPath(file);
            if (!project) {
                continue;
            }
            let record = projectMetadataMap.get(project);
            if (!record) {
                projectMetadataMap.set(project, (record = createInternalRecord(project, undefined, rootDir)));
            }
            record.hashes.set(file, hash);
        }
        let globalAdditionalHashes;
        if (globalAdditionalFiles) {
            globalAdditionalHashes = new Map();
            const sortedAdditionalFiles = Array.from(globalAdditionalFiles).sort();
            for (const file of sortedAdditionalFiles) {
                const hash = hashes.get(file);
                if (!hash) {
                    throw new Error(`Hash not found for global file: "${file}"`);
                }
                const owningProject = lookupByPath.findChildPath(file);
                if (owningProject) {
                    throw new InternalError(`Requested global additional file "${file}" is owned by project in "${owningProject.projectRelativeFolder}". Declare a project dependency instead.`);
                }
                globalAdditionalHashes.set(file, hash);
            }
        }
        for (const record of projectMetadataMap.values()) {
            // Ensure stable ordering.
            Sort.sortMapKeys(record.hashes);
        }
        this._projectMetadataMap = projectMetadataMap;
        this._additionalHashes = additionalHashes;
        this._globalAdditionalHashes = globalAdditionalHashes;
        // Snapshot the environment so that queries are not impacted by when they happen
        this._environment = environment;
        this.hashes = hashes;
        this.hasUncommittedChanges = hasUncommittedChanges;
        this.rootDirectory = rootDir;
    }
    /**
     * {@inheritdoc}
     */
    getTrackedFileHashesForOperation(project, operationName) {
        var _a, _b;
        const record = this._projectMetadataMap.get(project);
        if (!record) {
            throw new InternalError(`No information available for project at ${project.projectFolder}`);
        }
        const { fileHashesByOperationName } = record;
        let hashes = fileHashesByOperationName.get(operationName);
        if (!hashes) {
            hashes = new Map();
            fileHashesByOperationName.set(operationName, hashes);
            // TODO: Support incrementalBuildIgnoredGlobs per-operation
            const filter = getOrCreateProjectFilter(record);
            let outputValidator;
            if (operationName) {
                const operationSettings = (_a = record.projectConfig) === null || _a === void 0 ? void 0 : _a.operationSettingsByOperationName.get(operationName);
                const outputFolderNames = operationSettings === null || operationSettings === void 0 ? void 0 : operationSettings.outputFolderNames;
                if (outputFolderNames) {
                    const { relativePrefix } = record;
                    outputValidator = new LookupByPath();
                    for (const folderName of outputFolderNames) {
                        outputValidator.setItem(`${relativePrefix}/${folderName}`, folderName);
                    }
                }
                // Hash any additional files (files outside of a project, untracked project files, or even files outside of the repository)
                const additionalFilesForOperation = (_b = record.additionalFilesByOperationName) === null || _b === void 0 ? void 0 : _b.get(operationName);
                if (additionalFilesForOperation) {
                    for (const [filePath, hash] of this._resolveHashes(additionalFilesForOperation)) {
                        hashes.set(filePath, hash);
                    }
                }
            }
            const { _globalAdditionalHashes: globalAdditionalHashes } = this;
            if (globalAdditionalHashes) {
                for (const [file, hash] of globalAdditionalHashes) {
                    record.hashes.set(file, hash);
                }
            }
            // Hash the base project files
            for (const [filePath, hash] of record.hashes) {
                if (filter(filePath)) {
                    hashes.set(filePath, hash);
                }
                // Ensure that the configured output folders for this operation do not contain any input files
                // This should be reworked to operate on a global file origin map to ensure a hashed input
                // is not a declared output of *any* operation.
                const outputMatch = outputValidator === null || outputValidator === void 0 ? void 0 : outputValidator.findChildPath(filePath);
                if (outputMatch) {
                    throw new Error(`Configured output folder "${outputMatch}" for operation "${operationName}" in project "${project.projectRelativeFolder}" contains tracked input file "${filePath}".` +
                        ` If it is intended that this operation modifies its own input files, modify the build process to emit a warning if the output version differs from the input, and remove the directory from "outputFolderNames".` +
                        ` This will ensure cache correctness. Otherwise, change the build process to output to a disjoint folder.`);
                }
            }
        }
        return hashes;
    }
    /**
     * {@inheritdoc}
     */
    getOperationOwnStateHash(project, operationName) {
        var _a;
        const record = this._projectMetadataMap.get(project);
        if (!record) {
            throw new Error(`No information available for project at ${project.projectFolder}`);
        }
        const { hashByOperationName } = record;
        let hash = hashByOperationName.get(operationName);
        if (!hash) {
            const hashes = this.getTrackedFileHashesForOperation(project, operationName);
            const hasher = createHash('sha1');
            // If this is for a specific operation, apply operation-specific options
            if (operationName) {
                const operationSettings = (_a = record.projectConfig) === null || _a === void 0 ? void 0 : _a.operationSettingsByOperationName.get(operationName);
                if (operationSettings) {
                    const { dependsOnEnvVars, outputFolderNames } = operationSettings;
                    if (dependsOnEnvVars) {
                        // As long as we enumerate environment variables in a consistent order, we will get a stable hash.
                        // Changing the order in rush-project.json will change the hash anyway since the file contents are part of the hash.
                        for (const envVar of dependsOnEnvVars) {
                            hasher.update(`${hashDelimiter}$${envVar}=${this._environment[envVar] || ''}`);
                        }
                    }
                    if (outputFolderNames) {
                        hasher.update(`${hashDelimiter}${JSON.stringify(outputFolderNames)}`);
                    }
                }
            }
            // Hash the base project files
            for (const [filePath, fileHash] of hashes) {
                hasher.update(`${hashDelimiter}${filePath}${hashDelimiter}${fileHash}`);
            }
            hash = hasher.digest('hex');
            hashByOperationName.set(operationName, hash);
        }
        return hash;
    }
    *_resolveHashes(filePaths) {
        var _a;
        const { hashes, _additionalHashes } = this;
        for (const filePath of filePaths) {
            const hash = (_a = hashes.get(filePath)) !== null && _a !== void 0 ? _a : _additionalHashes === null || _additionalHashes === void 0 ? void 0 : _additionalHashes.get(filePath);
            if (!hash) {
                throw new Error(`Could not find hash for file path "${filePath}"`);
            }
            yield [filePath, hash];
        }
    }
}
function getOrCreateProjectFilter(record) {
    var _a;
    if (!record.projectFilePathFilter) {
        const ignoredGlobs = (_a = record.projectConfig) === null || _a === void 0 ? void 0 : _a.incrementalBuildIgnoredGlobs;
        if (!ignoredGlobs || ignoredGlobs.length === 0) {
            record.projectFilePathFilter = noopFilter;
        }
        else {
            const ignorer = ignore();
            ignorer.add(ignoredGlobs);
            const prefixLength = record.relativePrefix.length + 1;
            record.projectFilePathFilter = function projectFilePathFilter(filePath) {
                return !ignorer.ignores(filePath.slice(prefixLength));
            };
        }
    }
    return record.projectFilePathFilter;
}
function createInternalRecord(project, baseRecord, rootDir) {
    return {
        // Data from the caller
        projectConfig: baseRecord === null || baseRecord === void 0 ? void 0 : baseRecord.projectConfig,
        additionalFilesByOperationName: baseRecord === null || baseRecord === void 0 ? void 0 : baseRecord.additionalFilesByOperationName,
        // Caches
        hashes: new Map(),
        hashByOperationName: new Map(),
        fileHashesByOperationName: new Map(),
        relativePrefix: getRelativePrefix(project, rootDir)
    };
}
function getRelativePrefix(project, rootDir) {
    return Path.convertToSlashes(path.relative(rootDir, project.projectFolder));
}
function noopFilter(filePath) {
    return true;
}
//# sourceMappingURL=InputsSnapshot.js.map