import * as plugins from './plugins.js';
import { ShellEnv } from './classes.shellenv.js';
import type { IShellEnvContructorOptions, TExecutor } from './classes.shellenv.js';
import { ShellLog } from './classes.shelllog.js';
import * as cp from 'child_process';

// -- interfaces --
export interface IExecResult {
  exitCode: number;
  stdout: string;
}

export interface IExecResultStreaming {
  childProcess: cp.ChildProcess;
  finalPromise: Promise<IExecResult>;
  kill: () => Promise<void>;
  terminate: () => Promise<void>;
  keyboardInterrupt: () => Promise<void>;
  customSignal: (signal: plugins.smartexit.TProcessSignal) => Promise<void>;
}

interface IExecOptions {
  commandString: string;
  silent?: boolean;
  strict?: boolean;
  streaming?: boolean;
  interactive?: boolean;
}

export class Smartshell {
  public shellEnv: ShellEnv;
  public smartexit = new plugins.smartexit.SmartExit();

  constructor(optionsArg: IShellEnvContructorOptions) {
    this.shellEnv = new ShellEnv(optionsArg);
  }

  /**
   * Executes a given command asynchronously.
   */
  private async _exec(options: IExecOptions): Promise<IExecResult | IExecResultStreaming | void> {
    if (options.interactive) {
      return await this._execInteractive({ commandString: options.commandString });
    }
    return await this._execCommand(options);
  }

  /**
   * Executes an interactive command.
   */
  private async _execInteractive(options: Pick<IExecOptions, 'commandString'>): Promise<void> {
    // Skip interactive execution in CI environments.
    if (process.env.CI) {
      return;
    }

    return new Promise<void>((resolve) => {
      const shell = cp.spawn(options.commandString, {
        stdio: 'inherit',
        shell: true,
        detached: true,
      });

      this.smartexit.addProcess(shell);

      shell.on('close', (code) => {
        console.log(`Interactive shell terminated with code ${code}`);
        this.smartexit.removeProcess(shell);
        resolve();
      });
    });
  }

  /**
   * Executes a command and returns either a non-streaming result or a streaming interface.
   */
  private async _execCommand(options: IExecOptions): Promise<IExecResult | IExecResultStreaming> {
    const commandToExecute = this.shellEnv.createEnvExecString(options.commandString);
    const shellLogInstance = new ShellLog();

    const execChildProcess = cp.spawn(commandToExecute, [], {
      shell: true,
      cwd: process.cwd(),
      env: process.env,
      detached: false,
    });

    this.smartexit.addProcess(execChildProcess);

    // Capture stdout and stderr output.
    execChildProcess.stdout.on('data', (data) => {
      if (!options.silent) {
        shellLogInstance.writeToConsole(data);
      }
      shellLogInstance.addToBuffer(data);
    });

    execChildProcess.stderr.on('data', (data) => {
      if (!options.silent) {
        shellLogInstance.writeToConsole(data);
      }
      shellLogInstance.addToBuffer(data);
    });

    // Wrap child process termination into a Promise.
    const childProcessEnded: Promise<IExecResult> = new Promise((resolve, reject) => {
      execChildProcess.on('exit', (code, signal) => {
        this.smartexit.removeProcess(execChildProcess);

        const execResult: IExecResult = {
          exitCode: typeof code === 'number' ? code : (signal ? 1 : 0),
          stdout: shellLogInstance.logStore.toString(),
        };

        if (options.strict && code !== 0) {
          reject(new Error(`Command "${options.commandString}" exited with code ${code}`));
        } else {
          resolve(execResult);
        }
      });

      execChildProcess.on('error', (error) => {
        this.smartexit.removeProcess(execChildProcess);
        reject(error);
      });
    });

    // If streaming mode is enabled, return a streaming interface immediately.
    if (options.streaming) {
      return {
        childProcess: execChildProcess,
        finalPromise: childProcessEnded,
        kill: async () => {
          console.log(`Running tree kill with SIGKILL on process ${execChildProcess.pid}`);
          await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGKILL');
        },
        terminate: async () => {
          console.log(`Running tree kill with SIGTERM on process ${execChildProcess.pid}`);
          await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGTERM');
        },
        keyboardInterrupt: async () => {
          console.log(`Running tree kill with SIGINT on process ${execChildProcess.pid}`);
          await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGINT');
        },
        customSignal: async (signal: plugins.smartexit.TProcessSignal) => {
          console.log(`Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`);
          await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, signal);
        },
      } as IExecResultStreaming;
    }

    // For non-streaming mode, wait for the process to complete.
    return await childProcessEnded;
  }

  public async exec(commandString: string): Promise<IExecResult> {
    return (await this._exec({ commandString })) as IExecResult;
  }

  public async execSilent(commandString: string): Promise<IExecResult> {
    return (await this._exec({ commandString, silent: true })) as IExecResult;
  }

  public async execStrict(commandString: string): Promise<IExecResult> {
    return (await this._exec({ commandString, strict: true })) as IExecResult;
  }

  public async execStrictSilent(commandString: string): Promise<IExecResult> {
    return (await this._exec({ commandString, silent: true, strict: true })) as IExecResult;
  }

  public async execStreaming(commandString: string, silent: boolean = false): Promise<IExecResultStreaming> {
    return (await this._exec({ commandString, silent, streaming: true })) as IExecResultStreaming;
  }

  public async execStreamingSilent(commandString: string): Promise<IExecResultStreaming> {
    return (await this._exec({ commandString, silent: true, streaming: true })) as IExecResultStreaming;
  }

  public async execInteractive(commandString: string): Promise<void> {
    await this._exec({ commandString, interactive: true });
  }

  public async execAndWaitForLine(
    commandString: string,
    regex: RegExp,
    silent: boolean = false
  ): Promise<void> {
    const execStreamingResult = await this.execStreaming(commandString, silent);
    return new Promise<void>((resolve) => {
      execStreamingResult.childProcess.stdout.on('data', (chunk: Buffer | string) => {
        const data = typeof chunk === 'string' ? chunk : chunk.toString();
        if (regex.test(data)) {
          resolve();
        }
      });
    });
  }

  public async execAndWaitForLineSilent(commandString: string, regex: RegExp): Promise<void> {
    return this.execAndWaitForLine(commandString, regex, true);
  }
}