import type {
  CancellationToken,
  WebviewView,
  WebviewViewResolveContext,
} from "vscode";

import * as vscode from "vscode";
import {
  generateNonce,
  getScriptTag,
  getStaticMeta,
} from "../utils/staticUris";
import { Disposable } from "../utils/dispose";
import type { InitService } from "../services/initService";
import { Logger } from "../utils";
import { STEP_DEFINITIONS, type InitStepError } from "../boot/baseStep";
import { LSP_COMMANDS, SIDEBAR_BUNDLE_DIR } from "../constants";
import type { SubmitInitFormMessage, Suggestion } from "@codeflash/types";
import { getRootWorkspaceFolder } from "../utils/rootWorkspace";
import { join } from "path";
import { EventTracker } from "../telemetry/posthog";

export type StepInfo = {
  status: "completed" | "failed" | "idle";
  icon?: string;
  title: string;
  description: string;
  error?: InitStepError;
};

export class InitWebviewProvider
  extends Disposable
  implements vscode.WebviewViewProvider
{
  private _view?: vscode.WebviewView | undefined;
  private stepsInfo: StepInfo[] = [];
  private logger: Logger = new Logger("InitWebviewProvider");
  private currentInitMessage: string = "Initializing";
  private pendingMessages: unknown[] = [];
  private viewLoaded: boolean = false;
  private installTerminal?: vscode.Terminal | undefined;

  constructor(
    private _extensionUri: vscode.Uri,
    private readonly initService: InitService,
  ) {
    super();
    this._disposables.push(this.logger);

    // keep them in the constructor, don't put them in the resolveWebviewView as they are mendatory for the initService to work properly, and the view might not be resolved right away.
    this.initService.setUpdateInitMessage((m) => this.updateInitMessage(m));
    this.initService.setUpdateStep((n, u) => this.updateStep(n, u));
    this.initService.setGetCurrentSteps(() => this.stepsInfo);
    this.initService.setSetSteps(this.getSetterStepsFunction());
    this.initService.setShowInitForm((s, e, p) => this.showInitForm(s, e, p));
    this.initService.setRenderSteps(() => this.renderSteps());
    this.initService.setShowLoadingView(() => this.showLoadingView());
    this.initService.setSetSessionRecording((s) => this.setSessionRecording(s));
    this.initService.setUpdateStepProgress((c, t) =>
      this.updateStepProgress(c, t),
    );
  }

  private updateInitMessage(message: string) {
    this.currentInitMessage = message;
    const msg = { command: "updateInitMessage", message };
    if (this._view) {
      this.sendMessageWhenViewLoaded(msg);
    } else {
      this.pendingMessages.push(msg);
    }
  }

  private getSetterStepsFunction() {
    return (setter: (current: StepInfo[]) => StepInfo[]) => {
      this.stepsInfo = setter(this.stepsInfo);
    };
  }

  private updateStep(number: number, update: Partial<StepInfo>) {
    const index = number - 1;
    const current = Object.assign({}, this.stepsInfo[index]);
    this.stepsInfo[index] = { ...current, ...update };
  }

  private renderSteps() {
    const msg = {
      command: "renderSteps",
      steps: this.stepsInfo,
    };
    if (this._view) {
      this.logger.info("Rendering steps");
      this.sendMessageWhenViewLoaded(msg);
    } else {
      this.pendingMessages.push(msg);
    }
  }

  private showLoadingView() {
    const msg = {
      command: "showLoadingView",
    };
    if (this._view) {
      this.sendMessageWhenViewLoaded(msg);
    } else {
      this.pendingMessages.push(msg);
    }
  }

  private setSessionRecording(status: "start" | "stop") {
    const msg = {
      command:
        status === "start" ? "startSessionRecording" : "stopSessionRecording",
      userId: EventTracker.uniqueId,
    };
    if (this._view) {
      this.sendMessageWhenViewLoaded(msg);
    } else {
      this.pendingMessages.push(msg);
    }
  }

  private updateStepProgress(currentStep: number, totalSteps: number) {
    const msg = {
      command: "updateStepProgress",
      currentStep,
      totalSteps,
      steps: STEP_DEFINITIONS,
    };
    if (this._view) {
      this.sendMessageWhenViewLoaded(msg);
    } else {
      this.pendingMessages.push(msg);
    }
  }

  private showInitForm(
    suggestions: Record<string, Suggestion>,
    error?: string,
    pyprojectPath?: string,
  ) {
    const msg = {
      command: "showInitForm",
      suggestions,
      errorDetails: error,
      pyprojectPath,
    };
    if (this._view) {
      this.sendMessageWhenViewLoaded(msg);
    } else {
      this.pendingMessages.push(msg);
    }
  }

  private sendMessageWhenViewLoaded(msg: unknown, maxRetries = 20) {
    if (maxRetries <= 0) {
      this.logger.error("Max retries reached for sending message to webview");
      return;
    }
    if (this.viewLoaded && this._view) {
      this._view.webview.postMessage(msg);
    } else {
      setTimeout(() => {
        this.sendMessageWhenViewLoaded(msg, maxRetries - 1);
      }, 200);
    }
  }

  resolveWebviewView(
    webviewView: WebviewView,

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    _context: WebviewViewResolveContext,
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    _token: CancellationToken,
  ): Thenable<void> | void {
    this._view = webviewView;
    this._view.webview.options = {
      enableScripts: true,
      localResourceRoots: [
        vscode.Uri.joinPath(this._extensionUri, "media"),
        vscode.Uri.joinPath(this._extensionUri, SIDEBAR_BUNDLE_DIR, "assets"),
      ],
    };

    const nonce = generateNonce();
    const metaTags = getStaticMeta(
      this._view.webview,
      this._extensionUri,
      nonce,
    );
    const scriptTag = getScriptTag(
      this._view.webview,
      this._extensionUri,
      nonce,
    );
    this._view.webview.html = this.getHtmlContent(metaTags, scriptTag);

    const listener = this._view.webview.onDidReceiveMessage((message) => {
      if (message.command === "restartCodeflash") {
        vscode.commands.executeCommand("codeflash.reloadExtension");
      } else if (message.command === "vscodeCommand") {
        vscode.commands.executeCommand(message.cmd, ...message.args);
      } else if (message.command === "runInstallCommand") {
        this.handleInstallCommand(message.installCommand);
      } else if (message.command === "cancelInstallCommand") {
        this.handleCancelInstall();
      } else if (message.command === "webviewLoaded") {
        this.viewLoaded = true;
        this.mustApplyPendingMessages();
      } else if (message.type === "submitInitForm") {
        const incomingMessage = message as SubmitInitFormMessage;
        this.handleSubmitInitForm(incomingMessage);
      }
    });
    this._disposables.push(listener);
  }

  private setInitFormLoadingState(isLoading: boolean) {
    this.sendMessageWhenViewLoaded({
      command: "setInitFormLoadingState",
      isLoading,
    });
  }

  private handleInstallCommand(installCommand: string) {
    const workspaceFolder = getRootWorkspaceFolder();
    const terminalOptions: vscode.TerminalOptions = {
      name: "Codeflash Install",
    };
    if (workspaceFolder) {
      terminalOptions.cwd = workspaceFolder.uri;
    }
    this.installTerminal = vscode.window.createTerminal(terminalOptions);
    this.installTerminal.show();
    this.installTerminal.sendText(installCommand);

    // Listen for terminal close to reload extension
    const disposable = vscode.window.onDidCloseTerminal((closedTerminal) => {
      if (closedTerminal === this.installTerminal) {
        disposable.dispose();
        this.installTerminal = undefined;
        // Reload extension after terminal closes to check if installation succeeded
        vscode.commands.executeCommand("codeflash.reloadExtension");
      }
    });
  }

  private handleCancelInstall() {
    if (this.installTerminal) {
      this.installTerminal.dispose();
      this.installTerminal = undefined;
      this.sendMessageWhenViewLoaded({ command: "installCancelled" });
    }
  }

  private async handleSubmitInitForm(incomingMessage: SubmitInitFormMessage) {
    if (!this.initService.completeInitResult.bootResult) {
      throw new Error("Boot result not available");
    }
    const lspClient =
      this.initService.completeInitResult.bootResult.lspService.getClient();
    this.setInitFormLoadingState(true);
    const { formData, pyprojectPath } = incomingMessage.payload;

    const workspaceRoot = getRootWorkspaceFolder()?.uri.fsPath || "";
    const defaultPyprojectPath = join(workspaceRoot, "pyproject.toml");

    let result:
      | {
          status: string;
          message?: string;
          field_errors?: Record<string, string>;
        }
      | undefined;

    try {
      result = await lspClient.sendRequest<{
        status: string;
        message?: string;
        field_errors?: Record<string, string>;
      }>(LSP_COMMANDS.WRITE_CONFIG, {
        config_file: pyprojectPath || defaultPyprojectPath,
        config: formData,
      });
      if (result.status !== "success") {
        vscode.window.showErrorMessage(
          result.message || "Failed to write configuration",
        );
        // Send field errors to the form if available
        if (result.field_errors) {
          // Pause pooling to prevent form re-render that would clear errors
          this.initService.setPausePooling(true);
          this.sendMessageWhenViewLoaded({
            command: "setInitFormFieldErrors",
            fieldErrors: result.field_errors,
          });
        }
      } else {
        // Only resume pooling on success
        this.initService.resumePooling();
      }
    } finally {
      this.setInitFormLoadingState(false);
      // reset the current rendered error to allow re-rendering the form again to show the user the same error if it still exists.
      // Only reset if there was no validation error (field_errors)
      if (result?.status === "success" || !result?.field_errors) {
        this.initService.currentRenderedError = "";
      }
    }
  }

  private applyPendingMessages() {
    this.pendingMessages.forEach((msg) => {
      this.sendMessageWhenViewLoaded(msg);
    });
    this.pendingMessages = [];
  }

  private mustApplyPendingMessages(maxRetries = 10) {
    if (this.pendingMessages.length === 0) {
      return;
    }
    if (this._view) {
      this.applyPendingMessages();
      return;
    }
    let retries = 0;
    const interval = setInterval(() => {
      if (this._view) {
        this.applyPendingMessages();
        clearInterval(interval);
      } else if (retries >= maxRetries) {
        clearInterval(interval);
      }
      retries++;
    }, 300);
  }

  private getHtmlContent(metaTags: string, scriptTag: string): string {
    return `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Codeflash</title>
    ${metaTags}
  </head>
  <body>
    <div id="initview-container" class="gradiantBg">
      <div class="header-container">
        <div class="header" id="codeflash-logo-header"></div>
        <button class="help-btn" id="help-btn" title="Open Codeflash Documentation">
          <span class="codicon codicon-question"></span>
        </button>
      </div>

      <div class="step-progress-container" id="step-progress"></div>

      <div class="init-content-wrapper" id="init-content">
        <div class="loading-wrapper" id="loading">
          <div class="spinner"></div>
          <p id="init-message">${this.currentInitMessage}</p>
          <div class="wave-wrapper">
            ${[...Array(5)]
              .map(
                (_, i) =>
                  `<div class="wave-bar" style="animation-delay:${i * 100}ms" ></div>`,
              )
              .join("")}
          </div>
        </div>

        <div class="timeline" id="steps-container"></div>
      </div>

      <div id="init-form-container"></div>
    </div>
    ${scriptTag}
  </body>
</html>
`;
  }

  override dispose() {
    super.dispose();
    this._view = undefined;
  }
}
