// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { StdioWritable, TextRewriterTransform, Colorize, ConsoleTerminalProvider, TerminalChunkKind } from '@rushstack/terminal';
import { StreamCollator } from '@rushstack/stream-collator';
import { NewlineKind, Async, InternalError, AlreadyReportedError } from '@rushstack/node-core-library';
import { AsyncOperationQueue } from './AsyncOperationQueue';
import { OperationStatus } from './OperationStatus';
import { OperationExecutionRecord } from './OperationExecutionRecord';
/**
 * Format "======" lines for a shell window with classic 80 columns
 */
const ASCII_HEADER_WIDTH = 79;
const prioritySort = (a, b) => {
    return a.criticalPathLength - b.criticalPathLength;
};
/**
 * Sorts operations lexicographically by their name.
 * @param a - The first operation to compare
 * @param b - The second operation to compare
 * @returns A comparison result: -1 if a < b, 0 if a === b, 1 if a > b
 */
function sortOperationsByName(a, b) {
    const aName = a.name;
    const bName = b.name;
    return aName === bName ? 0 : aName < bName ? -1 : 1;
}
/**
 * A class which manages the execution of a set of tasks with interdependencies.
 * Initially, and at the end of each task execution, all unblocked tasks
 * are added to a ready queue which is then executed. This is done continually until all
 * tasks are complete, or prematurely fails if any of the tasks fail.
 */
export class OperationExecutionManager {
    constructor(operations, options) {
        this._streamCollator_onWriterActive = (writer) => {
            if (writer) {
                this._completedOperations++;
                // Format a header like this
                //
                // ==[ @rushstack/the-long-thing ]=================[ 1 of 1000 ]==
                // leftPart: "==[ @rushstack/the-long-thing "
                const leftPart = Colorize.gray('==[') + ' ' + Colorize.cyan(writer.taskName) + ' ';
                const leftPartLength = 4 + writer.taskName.length + 1;
                // rightPart: " 1 of 1000 ]=="
                const completedOfTotal = `${this._completedOperations} of ${this._totalOperations}`;
                const rightPart = ' ' + Colorize.white(completedOfTotal) + ' ' + Colorize.gray(']==');
                const rightPartLength = 1 + completedOfTotal.length + 4;
                // middlePart: "]=================["
                const twoBracketsLength = 2;
                const middlePartLengthMinusTwoBrackets = Math.max(ASCII_HEADER_WIDTH - (leftPartLength + rightPartLength + twoBracketsLength), 0);
                const middlePart = Colorize.gray(']' + '='.repeat(middlePartLengthMinusTwoBrackets) + '[');
                this._terminal.writeStdoutLine('\n' + leftPart + middlePart + rightPart);
                if (!this._quietMode) {
                    this._terminal.writeStdoutLine('');
                }
            }
        };
        const { quietMode, debugMode, parallelism, allowOversubscription, inputsSnapshot, beforeExecuteOperationAsync: beforeExecuteOperation, afterExecuteOperationAsync: afterExecuteOperation, onOperationStatusChangedAsync: onOperationStatusChanged, beforeExecuteOperationsAsync: beforeExecuteOperations, createEnvironmentForOperation } = options;
        this._completedOperations = 0;
        this._quietMode = quietMode;
        this._hasAnyFailures = false;
        this._hasAnyNonAllowedWarnings = false;
        this._hasAnyAborted = false;
        this._parallelism = parallelism;
        this._allowOversubscription = allowOversubscription;
        this._beforeExecuteOperation = beforeExecuteOperation;
        this._afterExecuteOperation = afterExecuteOperation;
        this._beforeExecuteOperations = beforeExecuteOperations;
        this._createEnvironmentForOperation = createEnvironmentForOperation;
        this._onOperationStatusChanged = (record) => {
            if (record.status === OperationStatus.Ready) {
                this._executionQueue.assignOperations();
            }
            onOperationStatusChanged === null || onOperationStatusChanged === void 0 ? void 0 : onOperationStatusChanged(record);
        };
        // TERMINAL PIPELINE:
        //
        // streamCollator --> colorsNewlinesTransform --> StdioWritable
        //
        this._outputWritable = options.destination || StdioWritable.instance;
        this._colorsNewlinesTransform = new TextRewriterTransform({
            destination: this._outputWritable,
            normalizeNewlines: NewlineKind.OsDefault,
            removeColors: !ConsoleTerminalProvider.supportsColor
        });
        this._streamCollator = new StreamCollator({
            destination: this._colorsNewlinesTransform,
            onWriterActive: this._streamCollator_onWriterActive
        });
        this._terminal = this._streamCollator.terminal;
        // Convert the developer graph to the mutable execution graph
        const executionRecordContext = {
            streamCollator: this._streamCollator,
            onOperationStatusChanged: this._onOperationStatusChanged,
            createEnvironment: this._createEnvironmentForOperation,
            inputsSnapshot,
            debugMode,
            quietMode
        };
        // Sort the operations by name to ensure consistency and readability.
        const sortedOperations = Array.from(operations).sort(sortOperationsByName);
        let totalOperations = 0;
        const executionRecords = (this._executionRecords = new Map());
        for (const operation of sortedOperations) {
            const executionRecord = new OperationExecutionRecord(operation, executionRecordContext);
            executionRecords.set(operation, executionRecord);
            if (!executionRecord.silent) {
                // Only count non-silent operations
                totalOperations++;
            }
        }
        this._totalOperations = totalOperations;
        for (const [operation, record] of executionRecords) {
            for (const dependency of operation.dependencies) {
                const dependencyRecord = executionRecords.get(dependency);
                if (!dependencyRecord) {
                    throw new Error(`Operation "${record.name}" declares a dependency on operation "${dependency.name}" that is not in the set of operations to execute.`);
                }
                record.dependencies.add(dependencyRecord);
                dependencyRecord.consumers.add(record);
            }
        }
        // Ensure we compute the compute the state hashes for all operations before the runtime graph potentially mutates.
        if (inputsSnapshot) {
            for (const record of executionRecords.values()) {
                record.getStateHash();
            }
        }
        const executionQueue = new AsyncOperationQueue(this._executionRecords.values(), prioritySort);
        this._executionQueue = executionQueue;
    }
    /**
     * Executes all operations which have been registered, returning a promise which is resolved when all the
     * operations are completed successfully, or rejects when any operation fails.
     */
    async executeAsync(abortController) {
        var _a;
        this._completedOperations = 0;
        const totalOperations = this._totalOperations;
        const abortSignal = abortController.signal;
        if (!this._quietMode) {
            const plural = totalOperations === 1 ? '' : 's';
            this._terminal.writeStdoutLine(`Selected ${totalOperations} operation${plural}:`);
            const nonSilentOperations = [];
            for (const record of this._executionRecords.values()) {
                if (!record.silent) {
                    nonSilentOperations.push(record.name);
                }
            }
            nonSilentOperations.sort();
            for (const name of nonSilentOperations) {
                this._terminal.writeStdoutLine(`  ${name}`);
            }
            this._terminal.writeStdoutLine('');
        }
        this._terminal.writeStdoutLine(`Executing a maximum of ${this._parallelism} simultaneous processes...`);
        const maxParallelism = Math.min(totalOperations, this._parallelism);
        await ((_a = this._beforeExecuteOperations) === null || _a === void 0 ? void 0 : _a.call(this, this._executionRecords));
        // This function is a callback because it may write to the collatedWriter before
        // operation.executeAsync returns (and cleans up the writer)
        const onOperationCompleteAsync = async (record) => {
            var _a;
            // If the operation is not terminal, we should _only_ notify the queue to assign operations.
            if (!record.isTerminal) {
                this._executionQueue.assignOperations();
            }
            else {
                try {
                    await ((_a = this._afterExecuteOperation) === null || _a === void 0 ? void 0 : _a.call(this, record));
                }
                catch (e) {
                    this._reportOperationErrorIfAny(record);
                    record.error = e;
                    record.status = OperationStatus.Failure;
                }
                this._onOperationComplete(record);
            }
        };
        const onOperationStartAsync = async (record) => {
            var _a;
            return await ((_a = this._beforeExecuteOperation) === null || _a === void 0 ? void 0 : _a.call(this, record));
        };
        await Async.forEachAsync(this._executionQueue, async (record) => {
            if (abortSignal.aborted) {
                record.status = OperationStatus.Aborted;
                // Bypass the normal completion handler, directly mark the operation as aborted and unblock the queue.
                // We do this to ensure that we aren't messing with the stopwatch or terminal.
                this._hasAnyAborted = true;
                this._executionQueue.complete(record);
            }
            else {
                await record.executeAsync({
                    onStart: onOperationStartAsync,
                    onResult: onOperationCompleteAsync
                });
            }
        }, {
            allowOversubscription: this._allowOversubscription,
            concurrency: maxParallelism,
            weighted: true
        });
        const status = this._hasAnyFailures
            ? OperationStatus.Failure
            : this._hasAnyAborted
                ? OperationStatus.Aborted
                : this._hasAnyNonAllowedWarnings
                    ? OperationStatus.SuccessWithWarning
                    : OperationStatus.Success;
        return {
            operationResults: this._executionRecords,
            status
        };
    }
    _reportOperationErrorIfAny(record) {
        // Failed operations get reported, even if silent.
        // Generally speaking, silent operations shouldn't be able to fail, so this is a safety measure.
        let message = undefined;
        if (record.error) {
            if (!(record.error instanceof AlreadyReportedError)) {
                message = record.error.message;
            }
        }
        if (message) {
            // This creates the writer, so don't do this until needed
            record.collatedWriter.terminal.writeStderrLine(message);
            // Ensure that the summary isn't blank if we have an error message
            // If the summary already contains max lines of stderr, this will get dropped, so we hope those lines
            // are more useful than the final exit code.
            record.stdioSummarizer.writeChunk({
                text: `${message}\n`,
                kind: TerminalChunkKind.Stdout
            });
        }
    }
    /**
     * Handles the result of the operation and propagates any relevant effects.
     */
    _onOperationComplete(record) {
        const { runner, name, status, silent, _operationMetadataManager: operationMetadataManager } = record;
        const stopwatch = (operationMetadataManager === null || operationMetadataManager === void 0 ? void 0 : operationMetadataManager.tryRestoreStopwatch(record.stopwatch)) || record.stopwatch;
        switch (status) {
            /**
             * This operation failed. Mark it as such and all reachable dependents as blocked.
             */
            case OperationStatus.Failure: {
                // Failed operations get reported, even if silent.
                // Generally speaking, silent operations shouldn't be able to fail, so this is a safety measure.
                this._reportOperationErrorIfAny(record);
                // This creates the writer, so don't do this globally
                const { terminal } = record.collatedWriter;
                terminal.writeStderrLine(Colorize.red(`"${name}" failed to build.`));
                const blockedQueue = new Set(record.consumers);
                for (const blockedRecord of blockedQueue) {
                    if (blockedRecord.status === OperationStatus.Waiting) {
                        // Now that we have the concept of architectural no-ops, we could implement this by replacing
                        // {blockedRecord.runner} with a no-op that sets status to Blocked and logs the blocking
                        // operations. However, the existing behavior is a bit simpler, so keeping that for now.
                        if (!blockedRecord.silent) {
                            terminal.writeStdoutLine(`"${blockedRecord.name}" is blocked by "${name}".`);
                        }
                        blockedRecord.status = OperationStatus.Blocked;
                        this._executionQueue.complete(blockedRecord);
                        if (!blockedRecord.silent) {
                            // Only increment the count if the operation is not silent to avoid confusing the user.
                            // The displayed total is the count of non-silent operations.
                            this._completedOperations++;
                        }
                        for (const dependent of blockedRecord.consumers) {
                            blockedQueue.add(dependent);
                        }
                    }
                    else if (blockedRecord.status !== OperationStatus.Blocked) {
                        // It shouldn't be possible for operations to be in any state other than Waiting or Blocked
                        throw new InternalError(`Blocked operation ${blockedRecord.name} is in an unexpected state: ${blockedRecord.status}`);
                    }
                }
                this._hasAnyFailures = true;
                break;
            }
            /**
             * This operation was restored from the build cache.
             */
            case OperationStatus.FromCache: {
                if (!silent) {
                    record.collatedWriter.terminal.writeStdoutLine(Colorize.green(`"${name}" was restored from the build cache.`));
                }
                break;
            }
            /**
             * This operation was skipped via legacy change detection.
             */
            case OperationStatus.Skipped: {
                if (!silent) {
                    record.collatedWriter.terminal.writeStdoutLine(Colorize.green(`"${name}" was skipped.`));
                }
                break;
            }
            /**
             * This operation intentionally didn't do anything.
             */
            case OperationStatus.NoOp: {
                if (!silent) {
                    record.collatedWriter.terminal.writeStdoutLine(Colorize.gray(`"${name}" did not define any work.`));
                }
                break;
            }
            case OperationStatus.Success: {
                if (!silent) {
                    record.collatedWriter.terminal.writeStdoutLine(Colorize.green(`"${name}" completed successfully in ${stopwatch.toString()}.`));
                }
                break;
            }
            case OperationStatus.SuccessWithWarning: {
                if (!silent) {
                    record.collatedWriter.terminal.writeStderrLine(Colorize.yellow(`"${name}" completed with warnings in ${stopwatch.toString()}.`));
                }
                this._hasAnyNonAllowedWarnings = this._hasAnyNonAllowedWarnings || !runner.warningsAreAllowed;
                break;
            }
            case OperationStatus.Aborted: {
                this._hasAnyAborted || (this._hasAnyAborted = true);
                break;
            }
            default: {
                throw new InternalError(`Unexpected operation status: ${status}`);
            }
        }
        this._executionQueue.complete(record);
    }
}
//# sourceMappingURL=OperationExecutionManager.js.map