// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { pnpmSyncUpdateFileAsync, pnpmSyncCopyAsync } from 'pnpm-sync-lib';
import * as semver from 'semver';
import { Colorize } from '@rushstack/terminal';
import { AlreadyExistsBehavior, AlreadyReportedError, Async, FileConstants, FileSystem, JsonFile, JsonSchema } from '@rushstack/node-core-library';
import { PackageExtractor } from '@rushstack/package-extractor';
import { RushConstants } from '../logic/RushConstants';
import { PnpmSyncUtilities } from './PnpmSyncUtilities';
import { BaseLinkManager, SymlinkKind } from '../logic/base/BaseLinkManager';
import schema from '../schemas/rush-hotlink-state.schema.json';
import { PURGE_ACTION_NAME } from './actionNameConstants';
const PROJECT_LINKS_STATE_JSON_SCHEMA = JsonSchema.fromLoadedObject(schema);
export class HotlinkManager {
    constructor(options) {
        const { rushLinkStateFilePath, linksBySubspaceName } = options;
        this._rushLinkStateFilePath = rushLinkStateFilePath;
        this._linksBySubspaceName = linksBySubspaceName;
    }
    hasAnyHotlinksInSubspace(subspaceName) {
        var _a;
        return !!((_a = this._linksBySubspaceName.get(subspaceName)) === null || _a === void 0 ? void 0 : _a.length);
    }
    async _hardLinkToLinkedPackageAsync(terminal, sourcePath, targetFolder, lockfileId) {
        const logMessageCallback = (logMessageOptions) => {
            PnpmSyncUtilities.processLogMessage(logMessageOptions, terminal);
        };
        await pnpmSyncUpdateFileAsync({
            sourceProjectFolder: sourcePath,
            // TODO: Update pnpmSyncUpdateFileAsync to take an Iterable<string>
            targetFolders: Array.from(targetFolder),
            lockfileId,
            logMessageCallback
        });
        const pnpmSyncJsonPath = `${sourcePath}/${RushConstants.nodeModulesFolderName}/${RushConstants.pnpmSyncFilename}`;
        await pnpmSyncCopyAsync({
            pnpmSyncJsonPath,
            ensureFolderAsync: FileSystem.ensureFolderAsync,
            forEachAsyncWithConcurrency: Async.forEachAsync,
            getPackageIncludedFiles: PackageExtractor.getPackageIncludedFilesAsync,
            logMessageCallback
        });
    }
    async _modifyAndSaveLinkStateAsync(cb) {
        const newLinksBySubspaceName = await cb(this._linksBySubspaceName);
        this._linksBySubspaceName = newLinksBySubspaceName;
        const linkStateJson = {
            fileVersion: 0,
            linksBySubspace: Object.fromEntries(newLinksBySubspaceName)
        };
        await JsonFile.saveAsync(linkStateJson, this._rushLinkStateFilePath);
    }
    async purgeLinksAsync(terminal, subspaceName) {
        if (!this.hasAnyHotlinksInSubspace(subspaceName)) {
            return false;
        }
        const logMessageCallback = (logMessageOptions) => {
            PnpmSyncUtilities.processLogMessage(logMessageOptions, terminal);
        };
        await this._modifyAndSaveLinkStateAsync(async (linksBySubspaceName) => {
            var _a;
            const rushLinkFileState = (_a = linksBySubspaceName.get(subspaceName)) !== null && _a !== void 0 ? _a : [];
            await Async.forEachAsync(rushLinkFileState, async ({ linkedPackagePath, affectedPnpmVirtualStoreFolderPaths = [] }) => {
                await pnpmSyncUpdateFileAsync({
                    sourceProjectFolder: linkedPackagePath,
                    targetFolders: [],
                    lockfileId: subspaceName,
                    logMessageCallback
                });
                // pnpm will reuse packages in .pnpm directory, so we need to manually delete them before installation
                await Async.forEachAsync(affectedPnpmVirtualStoreFolderPaths, async (affectedPnpmVirtualStoreFolderName) => {
                    await FileSystem.deleteFolderAsync(affectedPnpmVirtualStoreFolderName);
                }, { concurrency: 10 });
            }, { concurrency: 10 });
            const newLinksBySubspaceName = new Map(linksBySubspaceName);
            newLinksBySubspaceName.delete(subspaceName);
            return newLinksBySubspaceName;
        });
        return true;
    }
    async _getLinkedPackageInfoAsync(linkedPackagePath) {
        const linkedPackageJsonPath = `${linkedPackagePath}/${FileConstants.PackageJson}`;
        const linkedPackageJsonExists = await FileSystem.existsAsync(linkedPackageJsonPath);
        if (!linkedPackageJsonExists) {
            throw new Error(`Cannot find ${FileConstants.PackageJson} in the path ${linkedPackagePath}`);
        }
        const { dependencies = {}, name: packageName, peerDependencies = {} } = await JsonFile.loadAsync(linkedPackageJsonPath);
        const linkedPackageNodeModulesPath = `${linkedPackagePath}/${RushConstants.nodeModulesFolderName}`;
        const externalDependencies = [];
        const workspaceDependencies = [];
        for (const [name, protocol] of Object.entries(dependencies)) {
            if (protocol.startsWith('workspace')) {
                workspaceDependencies.push(name);
            }
            else {
                externalDependencies.push(name);
            }
        }
        return {
            packageName,
            linkedPackageNodeModulesPath,
            externalDependencies,
            workspaceDependencies,
            peerDependencies
        };
    }
    async _getPackagePathsMatchingNameAndVersionAsync(consumerPackagePnpmDependenciesFolderPath, packageName, versionRange) {
        const subDirectories = await FileSystem.readFolderItemNamesAsync(consumerPackagePnpmDependenciesFolderPath);
        const packageSourcePathSet = new Set();
        for (const dirName of subDirectories) {
            const packageSourcePath = `${consumerPackagePnpmDependenciesFolderPath}/${dirName}/${RushConstants.nodeModulesFolderName}/${packageName}`;
            if (await FileSystem.existsAsync(packageSourcePath)) {
                const { version } = await JsonFile.loadAsync(`${packageSourcePath}/${FileConstants.PackageJson}`);
                if (semver.satisfies(version, versionRange, { includePrerelease: true })) {
                    packageSourcePathSet.add(packageSourcePath);
                }
            }
        }
        return packageSourcePathSet;
    }
    async bridgePackageAsync(terminal, subspace, linkedPackagePath, version) {
        const subspaceName = subspace.subspaceName;
        try {
            const { packageName } = await this._getLinkedPackageInfoAsync(linkedPackagePath);
            const consumerPackagePnpmDependenciesFolderPath = `${subspace.getSubspaceTempFolderPath()}/${RushConstants.nodeModulesFolderName}/${RushConstants.pnpmVirtualStoreFolderName}`;
            const sourcePathSet = await this._getPackagePathsMatchingNameAndVersionAsync(consumerPackagePnpmDependenciesFolderPath, packageName, version);
            if (sourcePathSet.size === 0) {
                throw new Error(`Cannot find package ${packageName} ${version} in ${consumerPackagePnpmDependenciesFolderPath}`);
            }
            await this._hardLinkToLinkedPackageAsync(terminal, linkedPackagePath, sourcePathSet, subspaceName);
            await this._modifyAndSaveLinkStateAsync((linksBySubspaceName) => {
                var _a;
                const newConsumerPackageLinks = [
                    ...((_a = linksBySubspaceName.get(subspaceName)) !== null && _a !== void 0 ? _a : [])
                ];
                const existingLinkIndex = newConsumerPackageLinks.findIndex((link) => link.linkedPackageName === packageName);
                if (existingLinkIndex >= 0) {
                    newConsumerPackageLinks.splice(existingLinkIndex, 1);
                }
                newConsumerPackageLinks.push({
                    linkedPackagePath,
                    linkedPackageName: packageName,
                    affectedPnpmVirtualStoreFolderPaths: Array.from(sourcePathSet),
                    linkType: 'BridgePackage'
                });
                const newLinksBySubspaceName = new Map(linksBySubspaceName);
                newLinksBySubspaceName.set(subspaceName, newConsumerPackageLinks);
                return newLinksBySubspaceName;
            });
            terminal.writeLine(Colorize.green(`Successfully bridged package "${packageName}" for "${subspaceName}"`));
        }
        catch (error) {
            terminal.writeErrorLine(Colorize.red(`Failed to bridge package "${linkedPackagePath}" to "${subspaceName}": ${error.message}`));
            const alreadyExistsError = new AlreadyReportedError();
            alreadyExistsError.message = error.message;
            alreadyExistsError.stack = error.stack;
            throw alreadyExistsError;
        }
    }
    async linkPackageAsync(terminal, consumerPackage, linkedPackagePath) {
        const consumerPackageName = consumerPackage.packageName;
        try {
            const { packageName: linkedPackageName } = await this._getLinkedPackageInfoAsync(linkedPackagePath);
            const slashIndex = linkedPackageName.indexOf('/');
            const [scope, packageBaseName] = slashIndex !== -1
                ? [linkedPackageName.substring(0, slashIndex), linkedPackageName.substring(slashIndex)]
                : [undefined, linkedPackageName];
            let sourceNodeModulesPath = `${consumerPackage.projectFolder}/${RushConstants.nodeModulesFolderName}`;
            if (scope) {
                sourceNodeModulesPath = `${sourceNodeModulesPath}/${scope}`;
            }
            await FileSystem.ensureFolderAsync(sourceNodeModulesPath);
            const symlinkPath = `${sourceNodeModulesPath}/${packageBaseName}`;
            // Create symlink to linkedPackage
            await BaseLinkManager._createSymlinkAsync({
                symlinkKind: SymlinkKind.Directory,
                linkTargetPath: linkedPackagePath,
                newLinkPath: symlinkPath,
                alreadyExistsBehavior: AlreadyExistsBehavior.Overwrite
            });
            // Record the link information between the consumer package and the linked package
            await this._modifyAndSaveLinkStateAsync((linksBySubspaceName) => {
                var _a;
                const subspaceName = consumerPackage.subspace.subspaceName;
                const newConsumerPackageLinks = [
                    ...((_a = linksBySubspaceName.get(subspaceName)) !== null && _a !== void 0 ? _a : [])
                ];
                const existingLinkIndex = newConsumerPackageLinks.findIndex((link) => link.linkedPackageName === linkedPackageName);
                if (existingLinkIndex >= 0) {
                    newConsumerPackageLinks.splice(existingLinkIndex, 1);
                }
                newConsumerPackageLinks.push({
                    linkedPackagePath,
                    linkedPackageName,
                    linkType: 'LinkPackage'
                });
                const newLinksBySubspaceName = new Map(linksBySubspaceName);
                newLinksBySubspaceName.set(subspaceName, newConsumerPackageLinks);
                return newLinksBySubspaceName;
            });
            terminal.writeLine(Colorize.green(`Successfully linked package "${linkedPackageName}" for "${consumerPackageName}"`));
        }
        catch (error) {
            terminal.writeErrorLine(Colorize.red(`Failed to link package "${linkedPackagePath}" to "${consumerPackageName}": ${error.message}`));
            const alreadyExistsError = new AlreadyReportedError();
            alreadyExistsError.message = error.message;
            alreadyExistsError.stack = error.stack;
            throw alreadyExistsError;
        }
    }
    static loadFromRushConfiguration(rushConfiguration) {
        // TODO: make this function async
        const rushLinkStateFilePath = `${rushConfiguration.commonTempFolder}/${RushConstants.rushHotlinkStateFilename}`;
        let rushLinkState;
        try {
            rushLinkState = JsonFile.loadAndValidate(rushLinkStateFilePath, PROJECT_LINKS_STATE_JSON_SCHEMA);
        }
        catch (error) {
            if (!FileSystem.isNotExistError(error)) {
                throw error;
            }
        }
        if (!rushLinkState) {
            return new HotlinkManager({
                rushLinkStateFilePath,
                linksBySubspaceName: new Map()
            });
        }
        else {
            const { fileVersion, linksBySubspace } = rushLinkState;
            if (fileVersion !== 0) {
                throw new Error(`The rush project link state file "${rushLinkStateFilePath}" has an unexpected format, so this repo's ` +
                    `installation state is likely in an inconsistent state. Run 'rush ${PURGE_ACTION_NAME}' purge to clear ` +
                    `the installation.`);
            }
            else {
                const linksBySubspaceName = new Map(Object.entries(linksBySubspace));
                return new HotlinkManager({
                    rushLinkStateFilePath,
                    linksBySubspaceName
                });
            }
        }
    }
}
//# sourceMappingURL=HotlinkManager.js.map