// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { once } from 'node:events';
import { TerminalProviderSeverity } from '@rushstack/terminal';
import { EnvironmentConfiguration } from '../../api/EnvironmentConfiguration';
import { Utilities } from '../../utilities/Utilities';
import { OperationError } from './OperationError';
import { OperationStatus } from './OperationStatus';
function isAfterExecuteEventMessage(message) {
    return typeof message === 'object' && message.event === 'after-execute';
}
function isRequestRunEventMessage(message) {
    return typeof message === 'object' && message.event === 'requestRun';
}
function isSyncEventMessage(message) {
    return typeof message === 'object' && message.event === 'sync';
}
/**
 * Runner that hosts a long-lived process to which it communicates via IPC.
 */
export class IPCOperationRunner {
    constructor(options) {
        this.cacheable = false;
        this.reportTiming = true;
        this.silent = false;
        this.name = options.name;
        this.warningsAreAllowed =
            EnvironmentConfiguration.allowWarningsInSuccessfulBuild ||
                options.phase.allowWarningsOnSuccess ||
                false;
        this._rushProject = options.project;
        this._commandToRun = options.commandToRun;
        this._commandForHash = options.commandForHash;
        this._persist = options.persist;
        this._requestRun = options.requestRun;
        this._ignoredParameterValues = options.ignoredParameterValues;
    }
    async executeAsync(context) {
        return await context.runWithTerminalAsync(async (terminal, terminalProvider) => {
            var _a, _b;
            let isConnected = false;
            if (!this._ipcProcess || typeof this._ipcProcess.exitCode === 'number') {
                // Log any ignored parameters
                if (this._ignoredParameterValues.length > 0) {
                    terminal.writeLine(`These parameters were ignored for this operation by project-level configuration: ${this._ignoredParameterValues.join(' ')}`);
                }
                // Run the operation
                terminal.writeLine('Invoking: ' + this._commandToRun);
                const { rushConfiguration, projectFolder } = this._rushProject;
                const { environment: initialEnvironment } = context;
                this._ipcProcess = Utilities.executeLifecycleCommandAsync(this._commandToRun, {
                    rushConfiguration,
                    workingDirectory: projectFolder,
                    initCwd: rushConfiguration.commonTempFolder,
                    handleOutput: true,
                    environmentPathOptions: {
                        includeProjectBin: true
                    },
                    ipc: true,
                    connectSubprocessTerminator: true,
                    initialEnvironment
                });
                let resolveReadyPromise;
                this._processReadyPromise = new Promise((resolve) => {
                    resolveReadyPromise = resolve;
                });
                this._ipcProcess.on('message', (message) => {
                    if (isRequestRunEventMessage(message)) {
                        this._requestRun(message.requestor, message.detail);
                    }
                    else if (isSyncEventMessage(message)) {
                        resolveReadyPromise();
                    }
                });
            }
            else {
                terminal.writeLine(`Connecting to existing IPC process...`);
            }
            const subProcess = this._ipcProcess;
            let hasWarningOrError = false;
            function onStdout(data) {
                const text = data.toString();
                terminalProvider.write(text, TerminalProviderSeverity.log);
            }
            function onStderr(data) {
                const text = data.toString();
                terminalProvider.write(text, TerminalProviderSeverity.error);
                hasWarningOrError = true;
            }
            // Hook into events, in order to get live streaming of the log
            (_a = subProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', onStdout);
            (_b = subProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', onStderr);
            const status = await new Promise((resolve, reject) => {
                function finishHandler(message) {
                    var _a, _b;
                    if (isAfterExecuteEventMessage(message)) {
                        terminal.writeLine('Received finish notification');
                        (_a = subProcess.stdout) === null || _a === void 0 ? void 0 : _a.off('data', onStdout);
                        (_b = subProcess.stderr) === null || _b === void 0 ? void 0 : _b.off('data', onStderr);
                        subProcess.off('message', finishHandler);
                        subProcess.off('error', reject);
                        subProcess.off('exit', onExit);
                        terminal.writeLine('Disconnected from IPC process');
                        // These types are currently distinct but have the same underlying values
                        resolve(message.status);
                    }
                }
                function onExit(exitCode, signal) {
                    try {
                        if (signal) {
                            context.error = new OperationError('error', `Terminated by signal: ${signal}`);
                            resolve(OperationStatus.Failure);
                        }
                        else if (exitCode !== 0) {
                            // Do NOT reject here immediately, give a chance for other logic to suppress the error
                            context.error = new OperationError('error', `Returned error code: ${exitCode}`);
                            resolve(OperationStatus.Failure);
                        }
                        else if (hasWarningOrError) {
                            resolve(OperationStatus.SuccessWithWarning);
                        }
                        else {
                            resolve(OperationStatus.Success);
                        }
                    }
                    catch (error) {
                        reject(error);
                    }
                }
                subProcess.on('message', finishHandler);
                subProcess.on('error', reject);
                subProcess.on('exit', onExit);
                this._processReadyPromise.then(() => {
                    isConnected = true;
                    terminal.writeLine('Child supports IPC protocol. Sending "run" command...');
                    const runCommand = {
                        command: 'run'
                    };
                    subProcess.send(runCommand);
                }, reject);
            });
            if (isConnected && !this._persist) {
                await this.shutdownAsync();
            }
            // @rushstack/operation-graph does not currently have a concept of "Success with Warning"
            // To match existing ShellOperationRunner behavior we treat any stderr as a warning.
            return status === OperationStatus.Success && hasWarningOrError
                ? OperationStatus.SuccessWithWarning
                : status;
        }, {
            createLogFile: true
        });
    }
    getConfigHash() {
        return this._commandForHash;
    }
    async shutdownAsync() {
        const { _ipcProcess: subProcess } = this;
        if (!subProcess) {
            return;
        }
        if (subProcess.connected) {
            const exitCommand = {
                command: 'exit'
            };
            subProcess.send(exitCommand);
            await once(subProcess, 'exit');
        }
    }
}
//# sourceMappingURL=IPCOperationRunner.js.map