import * as child_process from "node:child_process";
import { isDeepStrictEqual } from "util";
import * as vscode from "vscode";

/**
 * Represents a running lldb-dap process that is accepting connections (i.e. in "server mode").
 *
 * Handles startup of the process if it isn't running already as well as prompting the user
 * to restart when arguments have changed.
 */
export class LLDBDapServer implements vscode.Disposable {
  private serverProcess?: child_process.ChildProcessWithoutNullStreams;
  private serverInfo?: Promise<{ host: string; port: number }>;

  /**
   * Starts the server with the provided options. The server will be restarted or reused as
   * necessary.
   *
   * @param dapPath the path to the debug adapter executable
   * @param args the list of arguments to provide to the debug adapter
   * @param options the options to provide to the debug adapter process
   * @returns a promise that resolves with the host and port information or `undefined` if unable to launch the server.
   */
  async start(
    dapPath: string,
    args: string[],
    options?: child_process.SpawnOptionsWithoutStdio,
  ): Promise<{ host: string; port: number } | undefined> {
    const dapArgs = [...args, "--connection", "listen://localhost:0" ];
    if (!(await this.shouldContinueStartup(dapPath, dapArgs))) {
      return undefined;
    }

    if (this.serverInfo) {
      return this.serverInfo;
    }

    this.serverInfo = new Promise((resolve, reject) => {
      const process = child_process.spawn(dapPath, dapArgs, options);
      process.on("error", (error) => {
        reject(error);
        this.serverProcess = undefined;
        this.serverInfo = undefined;
      });
      process.on("exit", (code, signal) => {
        let errorMessage = "Server process exited early";
        if (code !== undefined) {
          errorMessage += ` with code ${code}`;
        } else if (signal !== undefined) {
          errorMessage += ` due to signal ${signal}`;
        }
        reject(new Error(errorMessage));
        this.serverProcess = undefined;
        this.serverInfo = undefined;
      });
      process.stdout.setEncoding("utf8").on("data", (data) => {
        const connection = /connection:\/\/\[([^\]]+)\]:(\d+)/.exec(
          data.toString(),
        );
        if (connection) {
          const host = connection[1];
          const port = Number(connection[2]);
          resolve({ host, port });
          process.stdout.removeAllListeners();
        }
      });
      this.serverProcess = process;
    });
    return this.serverInfo;
  }

  /**
   * Checks to see if the server needs to be restarted. If so, it will prompt the user
   * to ask if they wish to restart.
   *
   * @param dapPath the path to the debug adapter
   * @param args the arguments for the debug adapter
   * @returns whether or not startup should continue depending on user input
   */
  private async shouldContinueStartup(
    dapPath: string,
    args: string[],
  ): Promise<boolean> {
    if (!this.serverProcess || !this.serverInfo) {
      return true;
    }

    if (isDeepStrictEqual(this.serverProcess.spawnargs, [dapPath, ...args])) {
      return true;
    }

    const userInput = await vscode.window.showInformationMessage(
      "The arguments to lldb-dap have changed. Would you like to restart the server?",
      {
        modal: true,
        detail: `An existing lldb-dap server (${this.serverProcess.pid}) is running with different arguments.

The previous lldb-dap server was started with:

${this.serverProcess.spawnargs.join(" ")}

The new lldb-dap server will be started with:

${dapPath} ${args.join(" ")}

Restarting the server will interrupt any existing debug sessions and start a new server.`,
      },
      "Restart",
      "Use Existing",
    );
    switch (userInput) {
      case "Restart":
        this.serverProcess.kill();
        this.serverProcess = undefined;
        this.serverInfo = undefined;
        return true;
      case "Use Existing":
        return true;
      case undefined:
        return false;
    }
  }

  dispose() {
    if (!this.serverProcess) {
      return;
    }
    this.serverProcess.kill();
    this.serverProcess = undefined;
    this.serverInfo = undefined;
  }
}
