import type {
  LanguageClientOptions,
  ServerOptions,
} from "vscode-languageclient/node";
import { State as LanguageClientState } from "vscode-languageclient/node";
import {
  CloseAction,
  ErrorAction,
  LanguageClient,
  TransportKind,
} from "vscode-languageclient/node";
import {
  LSP_CLIENT_ID,
  LSP_CLIENT_NAME,
  LSP_MODULE_NAME,
  OUTPUT_CHANNEL_PREFIX_NAMES,
} from "../constants";
import { Logger } from "../utils";
import { createInterceptingOutputChannel } from "../utils/outputChannelInterceptor";
import type { LogsEventEmitter } from "../utils/logsEventEmitter";

export const LSP_FAILED_MSG_TITLE = "Failed to start Codeflash LSP server";

export class LspService {
  private client: LanguageClient | undefined;
  private logger: Logger;
  private name: string;
  private startupError: Error | null = null;

  constructor(
    name: string,
    // the optional event emitter is for emitting events from the lsp server (via the output channel) to the sidebar provider for tracking the progress of the optimization
    private optimizationEventEmitter?: LogsEventEmitter,
  ) {
    this.name = name;
    this.logger = new Logger(
      OUTPUT_CHANNEL_PREFIX_NAMES.LSP + " - " + this.name,
    );
  }

  async start(pythonPath: string, emitLogs: boolean): Promise<LanguageClient> {
    var startTime = performance.now();
    if (this.client) {
      await this.stopClient();
    }

    const command = pythonPath;
    const args = ["-m", LSP_MODULE_NAME];

    this.logger.debug(
      `Starting CF-LSP with command: ${command} ${args.join(" ")}`,
    );
    this.logger.info(
      `Using Python module: ${LSP_MODULE_NAME}, with python path: ${pythonPath}`,
    );

    const serverOptions: ServerOptions = {
      run: {
        command,
        args,
        transport: TransportKind.stdio,
        options: {
          env: {
            ...process.env,
            CODEFLASH_LSP: "true",
          },
        },
      },
      debug: {
        transport: TransportKind.stdio,
        command,
        args: args,
        options: {
          env: {
            ...process.env,
            CODEFLASH_LSP: "true",
          },
        },
      },
    };

    if (emitLogs && !this.optimizationEventEmitter) {
      throw new Error(
        "emitLogs is true but optimizationEventEmitter is not provided",
      );
    }

    const channel = createInterceptingOutputChannel(
      this.logger._outputChannel,
      this.optimizationEventEmitter,
    );

    const clientOptions: LanguageClientOptions = {
      documentSelector: [{ scheme: "file", language: "python" }],
      synchronize: {},
      outputChannel: channel,
      traceOutputChannel: channel,
      errorHandler: {
        error: (error, message, count) => {
          this.startupError = error;
          this.logger.error(`[LSP ErrorHandler] Error #${count}: ${message}`);
          return { action: ErrorAction.Shutdown };
        },
        closed: () => {
          this.logger.error(`[LSP ErrorHandler] Connection closed`);
          return { action: CloseAction.DoNotRestart };
        },
      },
    };

    this.client = new LanguageClient(
      LSP_CLIENT_ID,
      LSP_CLIENT_NAME,
      serverOptions,
      clientOptions,
    );

    // Add event listeners for debugging
    this.client.onDidChangeState((event) => {
      this.logger.debug(
        `LSP client state changed: ${event.oldState} -> ${event.newState}`,
      );
    });

    this.client.onRequest("window/showMessage", (params) => {
      this.logger.debug(`LSP server message: ${params.message}`);
      return null;
    });

    this.client.onNotification("window/logMessage", (params) => {
      // Map LSP MessageType values to logger methods
      const messageTypeMap = {
        1: "error", // MessageType.Error
        2: "warn", // MessageType.Warning
        3: "info", // MessageType.Info
        4: "debug", // MessageType.Log
      };

      const logMethod =
        messageTypeMap[params.type as keyof typeof messageTypeMap] || "info";
      // Use dynamic method call to use the appropriate log level
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (this.logger as any)[logMethod](`[LSP] ${params.message}`);
    });

    this.logger.debug("Starting CF-LSP client...");

    try {
      const closedPromise = new Promise<never>((_, reject) => {
        this.client?.onDidChangeState((event) => {
          // 1 = Starting, 2 = Running, 3 = Stopped (disposed)
          if (event.newState === LanguageClientState.Stopped) {
            reject(
              this.startupError ??
                new Error(
                  channel.lastTracebackMessage ||
                    "LSP connection closed during startup",
                ),
            );
          }
        });
      });

      const startPromise = this.client.start();

      // whichever happens first: either it starts or it closes
      await Promise.race([startPromise, closedPromise]);

      if (this.client.state !== LanguageClientState.Running) {
        throw (
          this.startupError ??
          new Error(
            channel.lastTracebackMessage || "LSP failed to reach running state",
          )
        );
      }
      var endTime = performance.now();

      this.logger.debug(
        `CF-LSP client started successfully. State: ${this.client.state}, took ${endTime - startTime} ms`,
      );
      return this.client;
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      this.logger.error("Failed to start CF-LSP client : " + errorMessage);
      throw error;
    }
  }

  async stopClient(): Promise<void> {
    if (!this.client) {
      return;
    }
    this.logger.info("CF-LSP client stop requested.");
    const stopPromise = this.client.stop(5000);
    this.client = undefined;
    await stopPromise;
  }

  getClient(): LanguageClient {
    if (!this.client) {
      throw new Error("LSP client not initialized");
    }
    return this.client;
  }

  getClientState(): LanguageClientState | undefined {
    return this.client?.state;
  }

  dispose(): void {
    this.logger.dispose();
    this.stopClient();
  }
}
