// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as crypto from 'node:crypto';
import { DiscardStdoutTransform, SplitterTransform, StderrLineTransform, StdioSummarizer, ProblemCollector, TextRewriterTransform, Terminal } from '@rushstack/terminal';
import { InternalError, NewlineKind, FileError } from '@rushstack/node-core-library';
import { CollatedTerminal } from '@rushstack/stream-collator';
import { OperationStatus, TERMINAL_STATUSES } from './OperationStatus';
import { Stopwatch } from '../../utilities/Stopwatch';
import { OperationMetadataManager } from './OperationMetadataManager';
import { CollatedTerminalProvider } from '../../utilities/CollatedTerminalProvider';
import { RushConstants } from '../RushConstants';
import { getProjectLogFilePaths, initializeProjectLogFilesAsync } from './ProjectLogWritable';
/**
 * Internal class representing everything about executing an operation
 *
 * @internal
 */
export class OperationExecutionRecord {
    constructor(operation, context) {
        /**
         * The error which occurred while executing this operation, this is stored in case we need
         * it later (for example to re-print errors at end of execution).
         */
        this.error = undefined;
        /**
         * This number represents how far away this Operation is from the furthest "root" operation (i.e.
         * an operation with no consumers). This helps us to calculate the critical path (i.e. the
         * longest chain of projects which must be executed in order, thereby limiting execution speed
         * of the entire operation tree.
         *
         * This number is calculated via a memoized depth-first search, and when choosing the next
         * operation to execute, the operation with the highest criticalPathLength is chosen.
         *
         * Example:
         * ```
         *        (0) A
         *             \
         *          (1) B     C (0)         (applications)
         *               \   /|\
         *                \ / | \
         *             (2) D  |  X (1)      (utilities)
         *                    | / \
         *                    |/   \
         *                (2) Y     Z (2)   (other utilities)
         *
         * All roots (A & C) have a criticalPathLength of 0.
         * B has a score of 1, since A depends on it.
         * D has a score of 2, since we look at the longest chain (e.g D->B->A is longer than D->C)
         * X has a score of 1, since the only package which depends on it is A
         * Z has a score of 2, since only X depends on it, and X has a score of 1
         * Y has a score of 2, since the chain Y->X->C is longer than Y->C
         * ```
         *
         * The algorithm is implemented in AsyncOperationQueue.ts as calculateCriticalPathLength()
         */
        this.criticalPathLength = undefined;
        /**
         * The set of operations that must complete before this operation executes.
         */
        this.dependencies = new Set();
        /**
         * The set of operations that depend on this operation.
         */
        this.consumers = new Set();
        this.stopwatch = new Stopwatch();
        this.stdioSummarizer = new StdioSummarizer({
            // Allow writing to this object after transforms have been closed. We clean it up manually in a finally block.
            preventAutoclose: true
        });
        this.problemCollector = new ProblemCollector({
            matcherJson: [
                {
                    name: 'rushstack-file-error-unix',
                    pattern: FileError.getProblemMatcher({ format: 'Unix' })
                },
                {
                    name: 'rushstack-file-error-visualstudio',
                    pattern: FileError.getProblemMatcher({ format: 'VisualStudio' })
                }
            ]
        });
        this._collatedWriter = undefined;
        const { runner, associatedPhase, associatedProject } = operation;
        if (!runner) {
            throw new InternalError(`Operation for phase '${associatedPhase.name}' and project '${associatedProject.packageName}' has no runner.`);
        }
        this.operation = operation;
        this.runner = runner;
        this.associatedPhase = associatedPhase;
        this.associatedProject = associatedProject;
        this.logFilePaths = undefined;
        this._operationMetadataManager = new OperationMetadataManager({
            operation
        });
        this._context = context;
        this._status = operation.dependencies.size > 0 ? OperationStatus.Waiting : OperationStatus.Ready;
        this._stateHash = undefined;
        this._stateHashComponents = undefined;
    }
    get name() {
        return this.runner.name;
    }
    get weight() {
        return this.operation.weight;
    }
    get debugMode() {
        return this._context.debugMode;
    }
    get quietMode() {
        return this._context.quietMode;
    }
    get collatedWriter() {
        // Lazy instantiate because the registerTask() call affects display ordering
        if (!this._collatedWriter) {
            this._collatedWriter = this._context.streamCollator.registerTask(this.name);
        }
        return this._collatedWriter;
    }
    get nonCachedDurationMs() {
        var _a, _b;
        // Lazy calculated because the state file is created/restored later on
        return (_b = (_a = this._operationMetadataManager) === null || _a === void 0 ? void 0 : _a.stateFile.state) === null || _b === void 0 ? void 0 : _b.nonCachedDurationMs;
    }
    get cobuildRunnerId() {
        var _a, _b;
        // Lazy calculated because the state file is created/restored later on
        return (_b = (_a = this._operationMetadataManager) === null || _a === void 0 ? void 0 : _a.stateFile.state) === null || _b === void 0 ? void 0 : _b.cobuildRunnerId;
    }
    get environment() {
        var _a, _b;
        return (_b = (_a = this._context).createEnvironment) === null || _b === void 0 ? void 0 : _b.call(_a, this);
    }
    get metadataFolderPath() {
        var _a;
        return (_a = this._operationMetadataManager) === null || _a === void 0 ? void 0 : _a.metadataFolderPath;
    }
    get isTerminal() {
        return TERMINAL_STATUSES.has(this.status);
    }
    /**
     * The current execution status of an operation. Operations start in the 'ready' state,
     * but can be 'blocked' if an upstream operation failed. It is 'executing' when
     * the operation is executing. Once execution is complete, it is either 'success' or
     * 'failure'.
     */
    get status() {
        return this._status;
    }
    set status(newStatus) {
        var _a, _b;
        if (newStatus === this._status) {
            return;
        }
        this._status = newStatus;
        (_b = (_a = this._context).onOperationStatusChanged) === null || _b === void 0 ? void 0 : _b.call(_a, this);
    }
    get silent() {
        return !this.operation.enabled || this.runner.silent;
    }
    getStateHash() {
        if (this._stateHash === undefined) {
            const components = this.getStateHashComponents();
            const hasher = crypto.createHash('sha1');
            components.forEach((component) => {
                hasher.update(`${RushConstants.hashDelimiter}${component}`);
            });
            const hash = hasher.digest('hex');
            this._stateHash = hash;
        }
        return this._stateHash;
    }
    getStateHashComponents() {
        if (!this._stateHashComponents) {
            const { inputsSnapshot } = this._context;
            if (!inputsSnapshot) {
                throw new Error(`Cannot calculate state hash without git.`);
            }
            if (this.dependencies.size !== this.operation.dependencies.size) {
                throw new InternalError(`State hash calculation failed. Dependencies of record do not match the operation.`);
            }
            // The final state hashes of operation dependencies are factored into the hash to ensure that any
            // state changes in dependencies will invalidate the cache.
            const components = Array.from(this.dependencies, (record) => {
                return `${RushConstants.hashDelimiter}${record.name}=${record.getStateHash()}`;
            }).sort();
            const { associatedProject, associatedPhase } = this;
            // Examples of data in the local state hash:
            // - Environment variables specified in `dependsOnEnvVars`
            // - Git hashes of tracked files in the associated project
            // - Git hash of the shrinkwrap file for the project
            // - Git hashes of any files specified in `dependsOnAdditionalFiles` (must not be associated with a project)
            const localStateHash = inputsSnapshot.getOperationOwnStateHash(associatedProject, associatedPhase.name);
            components.push(`${RushConstants.hashDelimiter}local=${localStateHash}`);
            // Examples of data in the config hash:
            // - CLI parameters (ShellOperationRunner)
            const configHash = this.runner.getConfigHash();
            components.push(`${RushConstants.hashDelimiter}config=${configHash}`);
            this._stateHashComponents = components;
        }
        return this._stateHashComponents;
    }
    /**
     * {@inheritdoc IOperationRunnerContext.runWithTerminalAsync}
     */
    async runWithTerminalAsync(callback, options) {
        const { associatedProject, stdioSummarizer, problemCollector } = this;
        const { createLogFile, logFileSuffix = '' } = options;
        const logFilePaths = createLogFile
            ? getProjectLogFilePaths({
                project: associatedProject,
                logFilenameIdentifier: `${this._operationMetadataManager.logFilenameIdentifier}${logFileSuffix}`
            })
            : undefined;
        this.logFilePaths = logFilePaths;
        const projectLogWritable = logFilePaths
            ? await initializeProjectLogFilesAsync({
                logFilePaths,
                enableChunkedOutput: true
            })
            : undefined;
        try {
            //#region OPERATION LOGGING
            // TERMINAL PIPELINE:
            //
            //                             +--> quietModeTransform? --> collatedWriter
            //                             |
            // normalizeNewlineTransform --1--> stderrLineTransform --2--> projectLogWritable
            //                                                        |
            //                                                        +--> stdioSummarizer
            const destination = projectLogWritable
                ? new SplitterTransform({
                    destinations: [projectLogWritable, stdioSummarizer, problemCollector]
                })
                : stdioSummarizer;
            const stderrLineTransform = new StderrLineTransform({
                destination,
                newlineKind: NewlineKind.Lf // for StdioSummarizer
            });
            const splitterTransform1 = new SplitterTransform({
                destinations: [
                    this.quietMode
                        ? new DiscardStdoutTransform({ destination: this.collatedWriter })
                        : this.collatedWriter,
                    stderrLineTransform
                ]
            });
            const normalizeNewlineTransform = new TextRewriterTransform({
                destination: splitterTransform1,
                normalizeNewlines: NewlineKind.Lf,
                ensureNewlineAtEnd: true
            });
            const collatedTerminal = new CollatedTerminal(normalizeNewlineTransform);
            const terminalProvider = new CollatedTerminalProvider(collatedTerminal, {
                debugEnabled: this.debugMode
            });
            const terminal = new Terminal(terminalProvider);
            //#endregion
            const result = await callback(terminal, terminalProvider);
            normalizeNewlineTransform.close();
            // If the pipeline is wired up correctly, then closing normalizeNewlineTransform should
            // have closed projectLogWritable.
            if (projectLogWritable === null || projectLogWritable === void 0 ? void 0 : projectLogWritable.isOpen) {
                throw new InternalError('The output file handle was not closed');
            }
            return result;
        }
        finally {
            projectLogWritable === null || projectLogWritable === void 0 ? void 0 : projectLogWritable.close();
        }
    }
    async executeAsync({ onStart, onResult }) {
        var _a;
        if (!this.isTerminal) {
            this.stopwatch.reset();
        }
        this.stopwatch.start();
        this.status = OperationStatus.Executing;
        try {
            const earlyReturnStatus = await onStart(this);
            // When the operation status returns by the hook, bypass the runner execution.
            if (earlyReturnStatus) {
                this.status = earlyReturnStatus;
            }
            else {
                // If the operation is disabled, skip the runner and directly mark as Skipped.
                // However, if the operation is a NoOp, return NoOp so that cache entries can still be written.
                this.status = this.operation.enabled
                    ? await this.runner.executeAsync(this)
                    : this.runner.isNoOp
                        ? OperationStatus.NoOp
                        : OperationStatus.Skipped;
            }
            // Make sure that the stopwatch is stopped before reporting the result, otherwise endTime is undefined.
            this.stopwatch.stop();
            // Delegate global state reporting
            await onResult(this);
        }
        catch (error) {
            this.status = OperationStatus.Failure;
            this.error = error;
            // Make sure that the stopwatch is stopped before reporting the result, otherwise endTime is undefined.
            this.stopwatch.stop();
            // Delegate global state reporting
            await onResult(this);
        }
        finally {
            if (this.isTerminal) {
                (_a = this._collatedWriter) === null || _a === void 0 ? void 0 : _a.close();
                this.stdioSummarizer.close();
                this.problemCollector.close();
            }
        }
    }
}
//# sourceMappingURL=OperationExecutionRecord.js.map