/**
 * Copyright Microsoft Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import child_process from 'child_process';
import { EventEmitter } from 'events';
import { debug } from 'playwright-core/lib/utilsBundle';
import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc';
import type { ProtocolResponse } from '../common/process';
import { execArgvWithExperimentalLoaderOptions } from '../util';

export type ProcessExitData = {
  unexpectedly: boolean;
  code: number | null;
  signal: NodeJS.Signals | null;
};

export class ProcessHost extends EventEmitter {
  private process: child_process.ChildProcess | undefined;
  private _didSendStop = false;
  private _didFail = false;
  private didExit = false;
  private _runnerScript: string;
  private _lastMessageId = 0;
  private _callbacks = new Map<number, { resolve: (result: any) => void, reject: (error: Error) => void }>();
  private _processName: string;
  private _producedEnv: Record<string, string | undefined> = {};
  private _extraEnv: Record<string, string | undefined>;

  constructor(runnerScript: string, processName: string, env: Record<string, string | undefined>) {
    super();
    this._runnerScript = runnerScript;
    this._processName = processName;
    this._extraEnv = env;
  }

  async startRunner(runnerParams: any, inheritStdio: boolean) {
    this.process = child_process.fork(require.resolve('../common/process'), {
      detached: false,
      env: { ...process.env, ...this._extraEnv },
      stdio: inheritStdio ? ['ignore', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', process.env.PW_RUNNER_DEBUG ? 'inherit' : 'ignore', 'ipc'],
      ...(process.env.PW_TS_ESM_ON ? { execArgv: execArgvWithExperimentalLoaderOptions() } : {}),
    });
    this.process.on('exit', (code, signal) => {
      this.didExit = true;
      this.emit('exit', { unexpectedly: !this._didSendStop, code, signal } as ProcessExitData);
    });
    this.process.on('error', e => {});  // do not yell at a send to dead process.
    this.process.on('message', (message: any) => {
      if (debug.enabled('pw:test:protocol'))
        debug('pw:test:protocol')('◀ RECV ' + JSON.stringify(message));
      if (message.method === '__env_produced__') {
        const producedEnv: EnvProducedPayload = message.params;
        this._producedEnv = Object.fromEntries(producedEnv.map(e => [e[0], e[1] ?? undefined]));
      } else if (message.method === '__dispatch__') {
        const { id, error, method, params, result } = message.params as ProtocolResponse;
        if (id && this._callbacks.has(id)) {
          const { resolve, reject } = this._callbacks.get(id)!;
          this._callbacks.delete(id);
          if (error) {
            const errorObject = new Error(error.message);
            errorObject.stack = error.stack;
            reject(errorObject);
          } else {
            resolve(result);
          }
        } else {
          this.emit(method!, params);
        }
      } else {
        this.emit(message.method!, message.params);
      }
    });

    await new Promise<void>((resolve, reject) => {
      this.process!.once('exit', (code, signal) => reject(new Error(`process exited with code "${code}" and signal "${signal}" before it became ready`)));
      this.once('ready', () => resolve());
    });

    const processParams: ProcessInitParams = {
      stdoutParams: {
        rows: process.stdout.rows,
        columns: process.stdout.columns,
        colorDepth: process.stdout.getColorDepth?.() || 8
      },
      stderrParams: {
        rows: process.stderr.rows,
        columns: process.stderr.columns,
        colorDepth: process.stderr.getColorDepth?.() || 8
      },
      processName: this._processName
    };

    this.send({
      method: '__init__', params: {
        processParams,
        runnerScript: this._runnerScript,
        runnerParams
      }
    });
  }

  sendMessage(message: { method: string, params?: any }) {
    const id = ++this._lastMessageId;
    this.send({
      method: '__dispatch__',
      params: { id, ...message }
    });
    return new Promise((resolve, reject) => {
      this._callbacks.set(id, { resolve, reject });
    });
  }

  protected sendMessageNoReply(message: { method: string, params?: any }) {
    this.sendMessage(message).catch(() => {});
  }

  async stop(didFail?: boolean) {
    if (didFail)
      this._didFail = true;
    if (this.didExit)
      return;
    if (!this._didSendStop) {
      this.send({ method: '__stop__' });
      this._didSendStop = true;
    }
    await new Promise(f => this.once('exit', f));
  }

  didFail() {
    return this._didFail;
  }

  didSendStop() {
    return this._didSendStop;
  }

  producedEnv() {
    return this._producedEnv;
  }

  private send(message: { method: string, params?: any }) {
    if (debug.enabled('pw:test:protocol'))
      debug('pw:test:protocol')('SEND ► ' + JSON.stringify(message));
    this.process?.send(message);
  }
}
