import type {
  OutgoingWebviewMessage,
  RejectOptimizationMessage,
  ViewPatchMessage,
} from "@codeflash/types";
import {
  AnalysisService,
  LspService,
  NavigationService,
  OptimizationService,
} from "../services";
import { Logger } from "../utils";
import type { BootCodeflashServerStepResult } from "./baseStep";
import { InitStepError } from "./baseStep";
import BaseStep from "./baseStep";
import * as vscode from "vscode";
import { SidebarProvider } from "../providers/SidebarProvider";
import { CodeflashCodeLensProvider } from "../providers/CodeLensProvider";
import { GlobalStateKey, type GlobalState } from "../globalState";
import type { LanguageClient } from "vscode-languageclient/node";
import type { LogsEventEmitter } from "../utils/logsEventEmitter";
import { getLspServerFailedToStartError } from "./stepErrors";
import { CommentThreadProvider } from "../providers/commentThreadProvider";
import { GitWatcherService } from "../services/gitWatcherService";
import type { ConfigService } from "../services/configService";

export class BootCodeflashServerStep extends BaseStep {
  logger = new Logger("BootCodeflashServerStep");
  constructor(
    private readonly context: vscode.ExtensionContext,
    private readonly globalState: GlobalState,
    private readonly configService: ConfigService,
    private readonly pythonPath: string,
    private readonly optimizationEventEmitter: LogsEventEmitter,
  ) {
    super("Starting Codeflash server");
  }

  override async run(
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    ..._args: unknown[]
  ): Promise<BootCodeflashServerStepResult | InitStepError> {
    try {
      const { lspService, optimizationLspService } =
        await this.provideLspServices();
      const lspMainClient = lspService.getClient();
      const optimizationClient = optimizationLspService.getClient();
      if (!lspMainClient || !optimizationClient) {
        return new InitStepError("LSP client is not initialized");
      }
      const services = this.provideOtherServices(
        lspMainClient,
        optimizationClient,
      );
      this.registerGlobalLogsListener();
      return {
        lspService,
        optimizationLspService,
        ...services,
      };
    } catch (error) {
      if (error instanceof InitStepError) {
        return error;
      }
      return getLspServerFailedToStartError((error as Error).message);
    }
  }

  private provideLspServices = async (): Promise<{
    lspService: LspService;
    optimizationLspService: LspService;
  }> => {
    var startTime = performance.now();
    const lspService = new LspService("Main");

    // create another client for optimization as we are using a single pipe per client, so using a single client would be
    // mean waiting for the optimization to be finished before calling any other lsp features
    const optimizationLspService = new LspService(
      "Optimization",
      this.optimizationEventEmitter,
    );

    try {
      await Promise.all([
        lspService.start(this.pythonPath, false),
        optimizationLspService.start(this.pythonPath, true),
      ]);

      this.logger.debug("LSP client is ready and running.");

      var endTime = performance.now();
      this.logger.info(
        `Starting codeflash server took ${endTime - startTime} ms`,
      );

      this._disposables.push(lspService, optimizationLspService);

      return { lspService, optimizationLspService };
    } catch (error) {
      throw getLspServerFailedToStartError((error as Error).message);
    }
  };

  private provideOtherServices = (
    lspClient: LanguageClient,
    optimizationClient: LanguageClient,
  ): {
    analysisService: AnalysisService;
    optimizationService: OptimizationService;
    navigationService: NavigationService;
    sidebarProvider: SidebarProvider;
    codeLensProvider: CodeflashCodeLensProvider;
    commentThreadProvider: CommentThreadProvider;
    gitWatcherService: GitWatcherService;
  } => {
    const analysisService = new AnalysisService(lspClient);
    const optimizationService = new OptimizationService(optimizationClient);
    const navigationService = new NavigationService();

    const commentThreadProvider = new CommentThreadProvider(
      this.context,
      this.globalState,
      navigationService,
    );

    const sidebarProvider = new SidebarProvider(
      this.context.extensionUri,
      lspClient,
      optimizationService,
      analysisService,
      navigationService,
      this.optimizationEventEmitter,
      this.globalState,
      this.configService,
      commentThreadProvider,
      () => codeLensProvider.scheduleRefresh(),
    );
    const codeLensProvider = new CodeflashCodeLensProvider(
      lspClient,
      analysisService,
      navigationService,
      this.globalState,
    );

    // Register CodeLens provider for Python files
    const codeLensDisposable = vscode.languages.registerCodeLensProvider(
      { language: "python", scheme: "file" },
      codeLensProvider,
    );

    const gitWatcherService = new GitWatcherService(
      sidebarProvider,
      lspClient,
      this.configService,
    );
    this.logger.info("GitWatcherService initialized");

    this.provideCommands(sidebarProvider, commentThreadProvider);

    this._disposables.push(
      codeLensDisposable,
      analysisService,
      optimizationService,
      navigationService,
      sidebarProvider,
      codeLensProvider,
      commentThreadProvider,
      gitWatcherService,
    );
    return {
      analysisService,
      optimizationService,
      navigationService,
      sidebarProvider,
      codeLensProvider,
      commentThreadProvider,
      gitWatcherService,
    };
  };

  /**When
   * Safely register a VS Code command with error handling.
   * If a command already exists (e.g., during extension reload), log a warning
   * and return a no-op disposable to prevent crashes.
   */
  private safeRegisterCommand = (
    commandId: string,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    callback: (...args: any[]) => any,
  ): vscode.Disposable => {
    try {
      return vscode.commands.registerCommand(commandId, callback);
    } catch (error) {
      // Command already exists - this can happen during extension reload/retry
      this.logger.warn(
        `Command '${commandId}' already registered. This is expected during extension reload. Error: ${error}`,
      );
      // Return a no-op disposable to maintain the disposal chain
      return { dispose: () => {} };
    }
  };

  private provideCommands = (
    sidebarProvider: SidebarProvider,
    commentThreadProvider: CommentThreadProvider,
  ) => {
    const optimizeFunctionCommand = this.safeRegisterCommand(
      "codeflash.optimizeFunction",
      (uri: vscode.Uri, functionName: string) => {
        sidebarProvider.addFunctionToQueue(functionName, uri, true);
      },
    );
    const clearAllTasksCommand = this.safeRegisterCommand(
      "codeflash.clearAllTasks",
      () => {
        const allTasksIds = this.globalState
          .get(GlobalStateKey.QueueTasks, [])!
          .map((t) => t.id);
        for (const id of allTasksIds) {
          sidebarProvider.handleDeleteTask(id);
        }
        commentThreadProvider.scheduleRefresh();
      },
    );
    const optimizeAllCommand = this.safeRegisterCommand(
      "codeflash.optimizeAll",
      async () => {
        this.logger.info("Optimizing all functions from command");
        await sidebarProvider.handleOptimizeAllInCurrentFile();
      },
    );

    const viewOptimization = this.safeRegisterCommand(
      "codeflash.viewPatch",
      async (
        threadOrPayload:
          | ViewPatchMessage["payload"]
          | (vscode.CommentThread & {
              viewPatchPayload: ViewPatchMessage["payload"];
            }),
      ) => {
        let payload: ViewPatchMessage["payload"];

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        if ((threadOrPayload as any).viewPatchPayload) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          payload = (threadOrPayload as any).viewPatchPayload;
        } else {
          payload = threadOrPayload as ViewPatchMessage["payload"];
        }
        await sidebarProvider.handleViewPatch(payload);
      },
    );

    const acceptInlineCommentOptimizationCmd = this.safeRegisterCommand(
      "codeflash.acceptInlineRefactor",
      async (
        thread: vscode.CommentThread & {
          viewPatchPayload: ViewPatchMessage["payload"];
        },
      ) => {
        await sidebarProvider.applyPatchDirectlyForInlineThread(
          thread.viewPatchPayload,
        );
      },
    );

    const rejectInlineComnmentCommand = this.safeRegisterCommand(
      "codeflash.rejectInlineRefactor",
      async (
        thread: vscode.CommentThread & {
          rejectPayload: RejectOptimizationMessage["payload"];
        },
      ) => {
        const taskId = thread.rejectPayload.id;
        await sidebarProvider.rejectOptimization(taskId);
      },
    );

    const sendMessage = this.safeRegisterCommand(
      "codeflash.sendMessage",
      (message: OutgoingWebviewMessage) => {
        sidebarProvider.sendMessage(message);
      },
    );
    this._disposables.push(
      optimizeFunctionCommand,
      optimizeAllCommand,
      viewOptimization,
      sendMessage,
      clearAllTasksCommand,
      acceptInlineCommentOptimizationCmd,
      rejectInlineComnmentCommand,
    );
  };

  private registerGlobalLogsListener = () => {
    this._disposables.push(
      this.optimizationEventEmitter.registerLogsListener(),
    );
  };
}
