// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import * as http from "http";

import { ipcMain } from "electron";
import { firstValueFrom } from "rxjs";

import { SsoUrlService } from "@bitwarden/auth/common";
import { ClientType } from "@bitwarden/common/enums";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { MessageSender } from "@bitwarden/common/platform/messaging";

/**
 * The SSO Localhost login service uses a local host listener as fallback in case scheme handling deeplinks does not work.
 * This way it is possible to log in with SSO on appimage and electron dev using the same methods that the cli uses.
 */
export class SSOLocalhostCallbackService {
  private ssoRedirectUri = "";
  // We will only track one server at a time for use-case and performance considerations.
  // This will result in a last-one-wins behavior if multiple SSO flows are started simultaneously.
  private currentServer: http.Server | null = null;

  constructor(
    private environmentService: EnvironmentService,
    private messagingService: MessageSender,
    private ssoUrlService: SsoUrlService,
  ) {
    ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state, email }) => {
      // Close any existing server before starting new one
      if (this.currentServer) {
        await this.closeCurrentServer();
      }

      return this.openSsoPrompt(codeChallenge, state, email).then(({ ssoCode, recvState }) => {
        this.messagingService.send("ssoCallback", {
          code: ssoCode,
          state: recvState,
          redirectUri: this.ssoRedirectUri,
        });
      });
    });
  }

  private async closeCurrentServer(): Promise<void> {
    if (!this.currentServer) {
      return;
    }

    return new Promise<void>((resolve) => {
      this.currentServer!.close(() => {
        this.currentServer = null;
        resolve();
      });
    });
  }

  private async openSsoPrompt(
    codeChallenge: string,
    state: string,
    email: string,
  ): Promise<{ ssoCode: string; recvState: string }> {
    const env = await firstValueFrom(this.environmentService.environment$);

    return new Promise((resolve, reject) => {
      const callbackServer = http.createServer((req, res) => {
        const urlString = "http://localhost" + req.url;
        const url = new URL(urlString);
        const code = url.searchParams.get("code");
        if (code == null) {
          res.writeHead(404);
          res.end("not found");
          return;
        }
        const receivedState = url.searchParams.get("state");
        res.setHeader("Content-Type", "text/html");
        if (code != null && receivedState != null && this.checkState(receivedState, state)) {
          res.writeHead(200);
          res.end(
            "<html><head><title>Success | Bitwarden Desktop</title></head><body>" +
              "<h1>Successfully authenticated with the Bitwarden desktop app</h1>" +
              "<p>You may now close this tab and return to the app.</p>" +
              "</body></html>",
          );
          this.currentServer = null;
          callbackServer.close(() =>
            resolve({
              ssoCode: code,
              recvState: receivedState,
            }),
          );
        } else {
          res.writeHead(400);
          res.end(
            "<html><head><title>Failed | Bitwarden Desktop</title></head><body>" +
              "<h1>Something went wrong logging into the Bitwarden desktop app</h1>" +
              "<p>You may now close this tab and return to the app.</p>" +
              "</body></html>",
          );
          this.currentServer = null;
          callbackServer.close(() => reject());
        }
      });

      // Store reference to current server
      this.currentServer = callbackServer;

      const webUrl = env.getWebVaultUrl();

      const tryNextPort = (port: number) => {
        if (port > 8070) {
          this.currentServer = null;
          reject("All available SSO ports in use");
          return;
        }

        this.ssoRedirectUri = "http://localhost:" + port;
        const ssoUrl = this.ssoUrlService.buildSsoUrl(
          webUrl,
          ClientType.Desktop,
          this.ssoRedirectUri,
          state,
          codeChallenge,
          email,
        );

        // Set up error handler before attempting to listen
        callbackServer.once("error", (err: any) => {
          if (err.code === "EADDRINUSE") {
            // Port is in use, try next port
            tryNextPort(port + 1);
          } else {
            // Another error - reject and set the current server to null
            // (one server alive at a time)
            this.currentServer = null;
            reject();
          }
        });

        // Attempt to listen on the port
        callbackServer.listen(port, () => {
          // Success - remove error listener and launch SSO
          callbackServer.removeAllListeners("error");

          this.messagingService.send("launchUri", {
            url: ssoUrl,
          });
        });
      };

      // Start trying from port 8065
      tryNextPort(8065);

      // Don't allow any server to stay up for more than 5 minutes;
      // this gives plenty of time to complete SSO but ensures we don't
      // have a server running indefinitely.
      setTimeout(
        () => {
          if (this.currentServer === callbackServer) {
            this.currentServer = null;
          }
          callbackServer.close(() => reject());
        },
        5 * 60 * 1000,
      );
    });
  }

  private checkState(state: string, checkState: string): boolean {
    if (state === null || state === undefined) {
      return false;
    }
    if (checkState === null || checkState === undefined) {
      return false;
    }

    const stateSplit = state.split("_identifier=");
    const checkStateSplit = checkState.split("_identifier=");
    return stateSplit[0] === checkStateSplit[0];
  }
}
