// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { RushConstants } from '../RushConstants';
import { NullOperationRunner } from './NullOperationRunner';
import { Operation } from './Operation';
import { OperationStatus } from './OperationStatus';
import { getCustomParameterValuesByOperation, getDisplayName, initializeShellOperationRunner } from './ShellOperationRunnerPlugin';
export const PLUGIN_NAME = 'ShardedPhasedOperationPlugin';
// eslint-disable-next-line @typescript-eslint/typedef
const TemplateStrings = {
    SHARD_INDEX: '{shardIndex}',
    SHARD_COUNT: '{shardCount}',
    PHASE_NAME: '{phaseName}'
};
// eslint-disable-next-line @typescript-eslint/typedef
const TemplateStringRegexes = {
    SHARD_INDEX: new RegExp(TemplateStrings.SHARD_INDEX, 'g'),
    SHARD_COUNT: new RegExp(TemplateStrings.SHARD_COUNT, 'g'),
    PHASE_NAME: new RegExp(TemplateStrings.PHASE_NAME, 'g')
};
/**
 * Phased command that shards a phase into multiple operations.
 */
export class ShardedPhasedOperationPlugin {
    apply(hooks) {
        hooks.createOperations.tap(PLUGIN_NAME, spliceShards);
    }
}
function spliceShards(existingOperations, context) {
    var _a, _b, _c, _d;
    const { rushConfiguration, projectConfigurations } = context;
    const getCustomParameterValues = getCustomParameterValuesByOperation();
    for (const operation of existingOperations) {
        const { associatedPhase: phase, associatedProject: project, settings: operationSettings, logFilenameIdentifier: baseLogFilenameIdentifier } = operation;
        if ((operationSettings === null || operationSettings === void 0 ? void 0 : operationSettings.sharding) && !operation.runner) {
            const { count: shards } = operationSettings.sharding;
            /**
             * A single operation to reduce the number of edges in the graph when creating shards.
             * ```
             * depA -\          /- shard 1 -\
             * depB -- > noop < -- shard 2 -- > collator (reused operation)
             * depC -/          \- shard 3 -/
             * ```
             */
            const preShardOperation = new Operation({
                phase,
                project,
                settings: operationSettings,
                runner: new NullOperationRunner({
                    name: `${getDisplayName(phase, project)} - pre-shard`,
                    result: OperationStatus.NoOp,
                    silent: true
                }),
                logFilenameIdentifier: `${baseLogFilenameIdentifier}_pre-shard`
            });
            existingOperations.add(preShardOperation);
            for (const dependency of operation.dependencies) {
                preShardOperation.addDependency(dependency);
                operation.deleteDependency(dependency);
            }
            const outputFolderArgumentFormat = (_a = operationSettings.sharding.outputFolderArgumentFormat) !== null && _a !== void 0 ? _a : `--shard-output-directory=${RushConstants.projectRushFolderName}/operations/${TemplateStrings.PHASE_NAME}/shards/${TemplateStrings.SHARD_INDEX}`;
            if (!outputFolderArgumentFormat.includes('=')) {
                throw new Error('sharding.outputFolderArgumentFormat must contain an "=" sign to differentiate between the key and the value');
            }
            if (!outputFolderArgumentFormat.endsWith(TemplateStrings.SHARD_INDEX)) {
                throw new Error(`sharding.outputFolderArgumentFormat must end with ${TemplateStrings.SHARD_INDEX}, "${outputFolderArgumentFormat}"`);
            }
            // Replace the phase name only to begin with.
            const outputDirectoryArgument = outputFolderArgumentFormat.replace(TemplateStringRegexes.PHASE_NAME, baseLogFilenameIdentifier);
            const outputFolderWithTemplate = outputDirectoryArgument.substring(outputDirectoryArgument.indexOf('=') + 1);
            const parentFolder = outputFolderWithTemplate.substring(0, outputFolderWithTemplate.indexOf(TemplateStrings.SHARD_INDEX));
            const collatorDisplayName = `${getDisplayName(phase, project)} - collate`;
            // Get the custom parameter values for the collator, filtered according to the operation settings
            const { parameterValues: customParameterValues, ignoredParameterValues } = getCustomParameterValues(operation);
            const collatorParameters = [
                ...customParameterValues,
                `--shard-parent-folder="${parentFolder}"`,
                `--shard-count="${shards}"`
            ];
            const { scripts } = project.packageJson;
            const commandToRun = (_b = phase.shellCommand) !== null && _b !== void 0 ? _b : scripts === null || scripts === void 0 ? void 0 : scripts[phase.name];
            operation.logFilenameIdentifier = `${baseLogFilenameIdentifier}_collate`;
            operation.runner = initializeShellOperationRunner({
                phase,
                project,
                displayName: collatorDisplayName,
                rushConfiguration,
                commandToRun,
                customParameterValues: collatorParameters,
                ignoredParameterValues
            });
            const shardOperationName = `${phase.name}:shard`;
            const baseCommand = scripts === null || scripts === void 0 ? void 0 : scripts[shardOperationName];
            if (baseCommand === undefined) {
                throw new Error(`The project '${project.packageName}' does not define a '${phase.name}:shard' command in the 'scripts' section of its package.json`);
            }
            const shardArgumentFormat = (_c = operationSettings.sharding.shardArgumentFormat) !== null && _c !== void 0 ? _c : `--shard=${TemplateStrings.SHARD_INDEX}/${TemplateStrings.SHARD_COUNT}`;
            if (operationSettings.sharding.shardArgumentFormat &&
                !shardArgumentFormat.includes(TemplateStrings.SHARD_INDEX) &&
                !shardArgumentFormat.includes(TemplateStrings.SHARD_COUNT)) {
                throw new Error(`'shardArgumentFormat' must contain both ${TemplateStrings.SHARD_INDEX} and ${TemplateStrings.SHARD_COUNT} to be used for sharding.`);
            }
            const projectConfiguration = projectConfigurations.get(project);
            for (let shard = 1; shard <= shards; shard++) {
                const outputDirectory = outputFolderWithTemplate.replace(TemplateStringRegexes.SHARD_INDEX, shard.toString());
                const shardOperationSettings = (_d = projectConfiguration === null || projectConfiguration === void 0 ? void 0 : projectConfiguration.operationSettingsByOperationName.get(shardOperationName)) !== null && _d !== void 0 ? _d : operationSettings.sharding.shardOperationSettings;
                const shardOperation = new Operation({
                    project,
                    phase,
                    settings: {
                        ...shardOperationSettings,
                        operationName: shardOperationName,
                        outputFolderNames: [outputDirectory]
                    },
                    logFilenameIdentifier: `${baseLogFilenameIdentifier}_shard_${shard}`
                });
                const shardArgument = shardArgumentFormat
                    .replace(TemplateStringRegexes.SHARD_INDEX, shard.toString())
                    .replace(TemplateStringRegexes.SHARD_COUNT, shards.toString());
                const outputDirectoryArgumentWithShard = outputDirectoryArgument.replace(TemplateStringRegexes.SHARD_INDEX, shard.toString());
                const shardedParameters = [
                    ...customParameterValues,
                    shardArgument,
                    outputDirectoryArgumentWithShard
                ];
                const shardDisplayName = `${getDisplayName(phase, project)} - shard ${shard}/${shards}`;
                shardOperation.runner = initializeShellOperationRunner({
                    phase,
                    project,
                    commandToRun: baseCommand,
                    customParameterValues: shardedParameters,
                    displayName: shardDisplayName,
                    rushConfiguration,
                    ignoredParameterValues
                });
                shardOperation.addDependency(preShardOperation);
                operation.addDependency(shardOperation);
                existingOperations.add(shardOperation);
            }
        }
    }
    return existingOperations;
}
//# sourceMappingURL=ShardedPhaseOperationPlugin.js.map