// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { AlreadyReportedError } from '@rushstack/node-core-library';
import { Colorize } from '@rushstack/terminal';
import { BaseRushAction } from './BaseRushAction';
import { Event } from '../../api/EventHooks';
import { PurgeManager } from '../../logic/PurgeManager';
import { SetupChecks } from '../../logic/SetupChecks';
import { StandardScriptUpdater } from '../../logic/StandardScriptUpdater';
import { Stopwatch } from '../../utilities/Stopwatch';
import { VersionMismatchFinder } from '../../logic/versionMismatch/VersionMismatchFinder';
import { RushConstants } from '../../logic/RushConstants';
import { SUBSPACE_LONG_ARG_NAME } from '../parsing/SelectionParameterSet';
import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants';
import { measureAsyncFn } from '../../utilities/performance';
/**
 * This is the common base class for InstallAction and UpdateAction.
 */
export class BaseInstallAction extends BaseRushAction {
    constructor(options) {
        super(options);
        this._purgeParameter = this.defineFlagParameter({
            parameterLongName: '--purge',
            parameterShortName: '-p',
            description: 'Perform "rush purge" before starting the installation'
        });
        this._bypassPolicyParameter = this.defineFlagParameter({
            parameterLongName: RushConstants.bypassPolicyFlagLongName,
            description: `Overrides enforcement of the "gitPolicy" rules from ${RushConstants.rushJsonFilename} (use honorably!)`
        });
        this._noLinkParameter = this.defineFlagParameter({
            parameterLongName: '--no-link',
            description: 'If "--no-link" is specified, then project symlinks will NOT be created' +
                ' after the installation completes.  You will need to run "rush link" manually.' +
                ' This flag is useful for automated builds that want to report stages individually' +
                ' or perform extra operations in between the two stages. This flag is not supported' +
                ' when using workspaces.'
        });
        this._networkConcurrencyParameter = this.defineIntegerParameter({
            parameterLongName: '--network-concurrency',
            argumentName: 'COUNT',
            description: 'If specified, limits the maximum number of concurrent network requests.' +
                '  This is useful when troubleshooting network failures.'
        });
        this._debugPackageManagerParameter = this.defineFlagParameter({
            parameterLongName: '--debug-package-manager',
            description: 'Activates verbose logging for the package manager. You will probably want to pipe' +
                ' the output of Rush to a file when using this command.'
        });
        this._maxInstallAttempts = this.defineIntegerParameter({
            parameterLongName: '--max-install-attempts',
            argumentName: 'NUMBER',
            description: `Overrides the default maximum number of install attempts.`,
            defaultValue: RushConstants.defaultMaxInstallAttempts
        });
        this._ignoreHooksParameter = this.defineFlagParameter({
            parameterLongName: '--ignore-hooks',
            description: `Skips execution of the "eventHooks" scripts defined in ${RushConstants.rushJsonFilename}. ` +
                'Make sure you know what you are skipping.'
        });
        this._offlineParameter = this.defineFlagParameter({
            parameterLongName: '--offline',
            description: `Enables installation to be performed without internet access. PNPM will instead report an error` +
                ` if the necessary NPM packages cannot be obtained from the local cache.` +
                ` For details, see the documentation for PNPM's "--offline" parameter.`
        });
        this._variantParameter = this.defineStringParameter(VARIANT_PARAMETER);
    }
    async runAsync() {
        var _a, _b, _c;
        const installManagerOptions = await this.buildInstallOptionsAsync();
        // If we are doing a filtered install and subspaces is enabled, we need to find the affected subspaces and install for all of them.
        let selectedSubspaces;
        const subspaceInstallationDataBySubspace = new Map();
        if (this.rushConfiguration.subspacesFeatureEnabled) {
            // Selecting all subspaces if preventSelectingAllSubspaces is not enabled in subspaces.json
            if (((_a = this.rushConfiguration.subspacesConfiguration) === null || _a === void 0 ? void 0 : _a.preventSelectingAllSubspaces) &&
                !((_b = this._selectionParameters) === null || _b === void 0 ? void 0 : _b.didUserSelectAnything())) {
                this.terminal.writeLine();
                this.terminal.writeLine(Colorize.red(`The subspaces preventSelectingAllSubspaces configuration is enabled, which enforces installation for a specified set of subspace,` +
                    ` passed by the "${SUBSPACE_LONG_ARG_NAME}" parameter or selected from targeted projects using any project selector.`));
                throw new AlreadyReportedError();
            }
            const { selectedProjects } = installManagerOptions;
            if (selectedProjects.size === this.rushConfiguration.projects.length) {
                // Optimization for the common case, equivalent to the logic below
                selectedSubspaces = new Set(this.rushConfiguration.subspaces);
            }
            else {
                selectedSubspaces = this.rushConfiguration.getSubspacesForProjects(selectedProjects);
                for (const selectedSubspace of selectedSubspaces) {
                    let subspaceSelectedProjects;
                    let pnpmFilterArgumentValues;
                    if ((_c = selectedSubspace.getPnpmOptions()) === null || _c === void 0 ? void 0 : _c.alwaysFullInstall) {
                        subspaceSelectedProjects = new Set(selectedSubspace.getProjects());
                        pnpmFilterArgumentValues = [];
                    }
                    else {
                        // This may involve filtered installs. Go through each project, add its subspace's pnpm filter arguments
                        subspaceSelectedProjects = new Set();
                        pnpmFilterArgumentValues = [];
                        for (const project of selectedSubspace.getProjects()) {
                            if (selectedProjects.has(project)) {
                                subspaceSelectedProjects.add(project);
                                pnpmFilterArgumentValues.push(project.packageName);
                            }
                        }
                    }
                    subspaceInstallationDataBySubspace.set(selectedSubspace, {
                        selectedProjects: subspaceSelectedProjects,
                        pnpmFilterArgumentValues
                    });
                }
            }
        }
        const variant = await getVariantAsync(this._variantParameter, this.rushConfiguration, false);
        if (selectedSubspaces) {
            // Check each subspace for version inconsistencies
            for (const subspace of selectedSubspaces) {
                VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this.terminal, {
                    subspace,
                    variant
                });
            }
        }
        else {
            VersionMismatchFinder.ensureConsistentVersions(this.rushConfiguration, this.terminal, {
                subspace: undefined,
                variant
            });
        }
        const stopwatch = Stopwatch.start();
        SetupChecks.validate(this.rushConfiguration);
        let warnAboutScriptUpdate = false;
        if (this.actionName === 'update') {
            warnAboutScriptUpdate = await StandardScriptUpdater.updateAsync(this.rushConfiguration);
        }
        else {
            await StandardScriptUpdater.validateAsync(this.rushConfiguration);
        }
        this.eventHooksManager.handle(Event.preRushInstall, this.parser.isDebug, this._ignoreHooksParameter.value);
        const purgeManager = new PurgeManager(this.rushConfiguration, this.rushGlobalFolder);
        if (this._purgeParameter.value) {
            // eslint-disable-next-line no-console
            console.log('The --purge flag was specified, so performing "rush purge"');
            purgeManager.purgeNormal();
            // eslint-disable-next-line no-console
            console.log('');
        }
        if (this._networkConcurrencyParameter.value) {
            if (this.rushConfiguration.packageManager !== 'pnpm') {
                throw new Error(`The "${this._networkConcurrencyParameter.longName}" parameter is` +
                    ` only supported when using the PNPM package manager.`);
            }
        }
        if (this._maxInstallAttempts.value < 1) {
            throw new Error(`The value of "${this._maxInstallAttempts.longName}" must be positive and nonzero.`);
        }
        const installManagerFactoryModule = await import(
        /* webpackChunkName: 'InstallManagerFactory' */
        '../../logic/InstallManagerFactory');
        let installSuccessful = true;
        try {
            if (selectedSubspaces) {
                // Run the install for each affected subspace
                for (const subspace of selectedSubspaces) {
                    const subspaceInstallationData = subspaceInstallationDataBySubspace.get(subspace);
                    // eslint-disable-next-line no-console
                    console.log(Colorize.green(`Installing for subspace: ${subspace.subspaceName}`));
                    let installManagerOptionsForInstall;
                    if (subspaceInstallationData) {
                        // This will install the selected of projects in the subspace
                        const { selectedProjects, pnpmFilterArgumentValues } = subspaceInstallationData;
                        installManagerOptionsForInstall = {
                            ...installManagerOptions,
                            selectedProjects,
                            // IMPORTANT: SelectionParameterSet.getPnpmFilterArgumentValuesAsync() already calculated
                            // installManagerOptions.pnpmFilterArgumentValues using PNPM CLI operators such as "...my-app".
                            // But with subspaces, "pnpm install" can only see the subset of projects in subspace's temp workspace,
                            // therefore an operator like "--filter ...my-app" will malfunction.  As a workaround, here we are
                            // overwriting installManagerOptions.pnpmFilterArgumentValues with a flat last of project names that
                            // were calculated by Rush.
                            //
                            // TODO: If the flat list produces too many "--filter" arguments, invoking "pnpm install" will exceed
                            // the maximum command length and fail on Windows OS.  Once this is solved, we can eliminate the
                            // redundant logic from SelectionParameterSet.getPnpmFilterArgumentValuesAsync().
                            pnpmFilterArgumentValues,
                            subspace
                        };
                    }
                    else {
                        // This will install all projects in the subspace
                        installManagerOptionsForInstall = {
                            ...installManagerOptions,
                            pnpmFilterArgumentValues: [],
                            subspace
                        };
                    }
                    await this._doInstallAsync(installManagerFactoryModule, purgeManager, installManagerOptionsForInstall);
                }
            }
            else {
                // Simple case when subspacesFeatureEnabled=false
                await this._doInstallAsync(installManagerFactoryModule, purgeManager, {
                    ...installManagerOptions,
                    subspace: this.rushConfiguration.defaultSubspace
                });
            }
        }
        catch (error) {
            installSuccessful = false;
            throw error;
        }
        finally {
            await measureAsyncFn('rush:installManager:startDeleteAllAsync', () => purgeManager.startDeleteAllAsync());
            stopwatch.stop();
            this._collectTelemetry(stopwatch, installManagerOptions, installSuccessful);
            this.parser.flushTelemetry();
            this.eventHooksManager.handle(Event.postRushInstall, this.parser.isDebug, this._ignoreHooksParameter.value);
        }
        if (warnAboutScriptUpdate) {
            // eslint-disable-next-line no-console
            console.log('\n' +
                Colorize.yellow('Rush refreshed some files in the "common/scripts" folder.' +
                    '  Please commit this change to Git.'));
        }
        // eslint-disable-next-line no-console
        console.log('\n' + Colorize.green(`Rush ${this.actionName} finished successfully. (${stopwatch.toString()})`));
    }
    async _doInstallAsync(installManagerFactoryModule, purgeManager, installManagerOptions) {
        const installManager = await installManagerFactoryModule.InstallManagerFactory.getInstallManagerAsync(this.rushConfiguration, this.rushGlobalFolder, purgeManager, installManagerOptions);
        await measureAsyncFn('rush:installManager:doInstallAsync', () => installManager.doInstallAsync());
    }
    _collectTelemetry(stopwatch, installManagerOptions, success) {
        var _a;
        if (this.parser.telemetry) {
            const extraData = {
                mode: this.actionName,
                clean: (!!this._purgeParameter.value).toString(),
                debug: installManagerOptions.debug.toString(),
                full: installManagerOptions.fullUpgrade.toString(),
                ...this.getParameterStringMap(),
                ...(_a = this._selectionParameters) === null || _a === void 0 ? void 0 : _a.getTelemetry()
            };
            this.parser.telemetry.log({
                name: 'install',
                durationInSeconds: stopwatch.duration,
                result: success ? 'Succeeded' : 'Failed',
                extraData
            });
        }
    }
}
//# sourceMappingURL=BaseInstallAction.js.map