/* eslint-disable no-console */
import { get, setAPIKey } from "@toruslabs/http-helpers";
import { BasePostMessageStream, JRPCRequest, ObjectMultiplex, setupMultiplex, Substream } from "@toruslabs/openlogin-jrpc";
import deepmerge from "lodash.merge";

import configuration from "./config";
import Consent from "./consent";
import { documentReady, handleStream, htmlToElement, runOnLoad } from "./embedUtils";
import UpbondInpageProvider from "./inpage-provider";
import generateIntegrity from "./integrity";
import {
  BUTTON_POSITION,
  BUTTON_POSITION_TYPE,
  EMBED_TRANSLATION_ITEM,
  IUpbondEmbedParams,
  LOGIN_PROVIDER,
  NetworkInterface,
  PAYMENT_PROVIDER_TYPE,
  PaymentParams,
  TorusCtorArgs,
  TorusLoginParams,
  TorusPublicKey,
  UnvalidatedJsonRpcRequest,
  UPBOND_BUILD_ENV,
  UserInfo,
  VerifierArgs,
  WALLET_OPENLOGIN_VERIFIER_MAP,
  WALLET_PATH,
  WhiteLabelParams,
} from "./interfaces";
import log from "./loglevel";
import PopupHandler from "./PopupHandler";
import sendSiteMetadata from "./siteMetadata";
import {
  defaultLoginParam,
  defaultLoginParamProd,
  defaultLoginParamStg,
  FEATURES_CONFIRM_WINDOW,
  FEATURES_DEFAULT_WALLET_WINDOW,
  FEATURES_PROVIDER_CHANGE_WINDOW,
  getPreopenInstanceId,
  getUpbondWalletUrl,
  getUserLanguage,
  parseIdToken,
  searchToObject,
  validatePaymentProvider,
} from "./utils";

const defaultVerifiers = {
  [LOGIN_PROVIDER.GOOGLE]: true,
  [LOGIN_PROVIDER.FACEBOOK]: true,
  [LOGIN_PROVIDER.REDDIT]: true,
  [LOGIN_PROVIDER.TWITCH]: true,
  [LOGIN_PROVIDER.DISCORD]: true,
};

const iframeIntegrity = "sha384-RhqFseQpufEgNnJYPxNXx+EmyE55iWEWJwkgS7QX/pit6STKFZRf9K9kwmfpDIPw";

const expectedCacheControlHeader = "max-age=3600";

const UNSAFE_METHODS = [
  "eth_sendTransaction",
  "eth_signTypedData",
  "eth_signTypedData_v3",
  "eth_signTypedData_v4",
  "personal_sign",
  "eth_getEncryptionPublicKey",
  "eth_decrypt",
];

// preload for iframe doesn't work https://bugs.chromium.org/p/chromium/issues/detail?id=593267
(async function preLoadIframe() {
  try {
    if (typeof document === "undefined") return;
    const upbondIframeHtml = document.createElement("link");
    const { torusUrl } = await getUpbondWalletUrl("production", { check: false, hash: iframeIntegrity, version: "" });
    upbondIframeHtml.href = `${torusUrl}/popup`;
    upbondIframeHtml.crossOrigin = "anonymous";
    upbondIframeHtml.type = "text/html";
    upbondIframeHtml.rel = "prefetch";
    // if (upbondIframeHtml.relList && upbondIframeHtml.relList.supports) {
    //   if (upbondIframeHtml.relList.supports("prefetch")) {
    //     log.info("IFrame loaded");
    //     document.head.appendChild(upbondIframeHtml);
    //   }
    // }
  } catch (error) {
    log.warn(error);
  }
})();

export const ACCOUNT_TYPE = {
  NORMAL: "normal",
  THRESHOLD: "threshold",
  IMPORTED: "imported",
};

class Upbond {
  buttonPosition: BUTTON_POSITION_TYPE = BUTTON_POSITION.BOTTOM_LEFT;

  buttonSize: number;

  torusUrl: string;

  upbondIframe: HTMLIFrameElement;

  skipDialog: boolean;

  styleLink: HTMLLinkElement;

  isLoggedIn: boolean;

  isInitialized: boolean;

  torusAlert: HTMLDivElement;

  apiKey: string;

  modalZIndex: number;

  alertZIndex: number;

  torusAlertContainer: HTMLDivElement;

  isIframeFullScreen: boolean;

  whiteLabel: WhiteLabelParams;

  requestedVerifier: string;

  currentVerifier: string;

  embedTranslations: EMBED_TRANSLATION_ITEM;

  ethereum: UpbondInpageProvider;

  provider: UpbondInpageProvider;

  communicationMux: ObjectMultiplex;

  isUsingDirect: boolean;

  isLoginCallback: () => void;

  dappRedirectUrl: string;

  paymentProviders = configuration.paymentProviders;

  selectedVerifier: string;

  buildEnv: (typeof UPBOND_BUILD_ENV)[keyof typeof UPBOND_BUILD_ENV];

  widgetConfig: { showAfterLoggedIn: boolean; showBeforeLoggedIn: boolean };

  consent: Consent | null;

  consentConfiguration: {
    enable: boolean;
    config: {
      publicKey: string;
      scope: string[];
      origin: string;
    };
  };

  flowConfig: string;

  idToken: any;

  private loginHint = "";

  private useWalletConnect: boolean;

  private isCustomLogin = false;

  constructor(opts: TorusCtorArgs = {}) {
    this.buttonPosition = opts.buttonPosition || "bottom-left";
    this.buttonSize = opts.buttonSize || 56;
    this.torusUrl = "";
    this.isLoggedIn = false; // ethereum.enable working
    this.isInitialized = false; // init done
    this.requestedVerifier = "";
    this.currentVerifier = "";
    this.apiKey = opts.apiKey || "torus-default";
    setAPIKey(this.apiKey);
    this.modalZIndex = opts.modalZIndex || 999999;
    this.alertZIndex = this.modalZIndex + 1000;
    this.isIframeFullScreen = false;
    this.isUsingDirect = false;
    this.buildEnv = "production";
    this.widgetConfig = {
      showAfterLoggedIn: true,
      showBeforeLoggedIn: false,
    };
    this.idToken = "";
  }

  async init({
    buildEnv = UPBOND_BUILD_ENV.PRODUCTION,
    enableLogging = false,
    // deprecated: use loginConfig instead
    enabledVerifiers = defaultVerifiers,
    network = {
      host: "matic",
      chainId: 137,
      networkName: "Polygon",
      blockExplorer: "https://polygonscan.com/",
      ticker: "MATIC",
      tickerName: "MATIC",
      rpcUrl: "https://polygon-rpc.com",
    },
    loginConfig = defaultLoginParam,
    widgetConfig = this.widgetConfig, // default widget config
    integrity = {
      check: false,
      hash: iframeIntegrity,
      version: "",
    },
    whiteLabel,
    skipTKey = false,
    useWalletConnect = false,
    isUsingDirect = true,
    mfaLevel = "default",
    selectedVerifier,
    skipDialog = false,
    dappRedirectUri = window.location.origin,
    consentConfiguration = {
      enable: false,
      config: {
        publicKey: "",
        scope: [],
        origin: "",
      },
    },
    flowConfig = "normal",
    state = "",
  }: IUpbondEmbedParams = {}): Promise<void> {
    // Send WARNING for deprecated buildEnvs
    // Give message to use others instead
    let buildTempEnv = buildEnv;
    if (buildEnv === "v2_development") {
      console.log(
        `%c [UPBOND-EMBED] WARNING! This buildEnv is deprecating soon. Please use 'UPBOND_BUILD_ENV.DEVELOPMENT' instead to point wallet on DEVELOPMENT environment.`,
        "color: #FF0000"
      );
      console.log(`More information, please visit https://github.com/upbond/embed`);
      buildTempEnv = "development";
    }
    if (buildEnv === "v2_production") {
      console.log(
        `%c [UPBOND-EMBED] WARNING! This buildEnv is deprecating soon. Please use 'UPBOND_BUILD_ENV.PRODUCTION' instead to point wallet on PRODUCTION environment.`,
        "color: #FF0000"
      );
      console.log(`More information, please visit https://github.com/upbond/embed`);
      buildTempEnv = "production";
    }

    if (buildEnv.includes("v1")) {
      console.log(
        `%c [UPBOND-EMBED] WARNING! This buildEnv is deprecating soon. Please use 'UPBOND_BUILD_ENV.LOCAL|DEVELOPMENT|STAGING|PRODUCTION' instead to point wallet on each environment`,
        "color: #FF0000"
      );
      console.log(`More information, please visit https://github.com/upbond/embed`);
    }
    if (state) this.idToken = state;

    buildEnv = buildTempEnv;
    log.info(`Using buildEnv: `, buildEnv);

    // Enable/Disable logging
    if (enableLogging) log.enableAll();
    else log.disableAll();

    // Check env staging / production, set LoginConfig
    let loginConfigTemp = loginConfig;
    if (JSON.stringify(loginConfig) === JSON.stringify(defaultLoginParam)) {
      // For development, using the defaultloginparam
      // For staging, using defaultLoginParamStg
      if (buildEnv.includes("staging")) loginConfigTemp = defaultLoginParamStg;
      // For production, using defaultLoginParamProd
      if (buildEnv.includes("production")) loginConfigTemp = defaultLoginParamProd;
      loginConfig = loginConfigTemp;
    }
    log.info(`Using login config: `, loginConfig);
    log.info(`Using network config: `, network);

    if (this.isInitialized) throw new Error("Already initialized");

    const { torusUrl, logLevel } = await getUpbondWalletUrl(buildEnv, integrity);

    log.info(`Url Loaded: ${torusUrl} with log: ${logLevel}`);

    if (selectedVerifier) {
      try {
        const isAvailableOnLoginConfig = loginConfig[selectedVerifier];
        if (isAvailableOnLoginConfig) {
          this.selectedVerifier = selectedVerifier;
        } else {
          throw new Error("Selected verifier is not exist on your loginConfig");
        }
      } catch (error) {
        throw new Error("Selected verifier is not exist on your loginConfig");
      }
    }

    this.buildEnv = buildEnv;
    this.skipDialog = skipDialog;
    this.dappRedirectUrl = dappRedirectUri;
    this.isUsingDirect = isUsingDirect;
    this.torusUrl = torusUrl;
    this.whiteLabel = whiteLabel;
    this.useWalletConnect = useWalletConnect;
    this.isCustomLogin = !!(loginConfig && Object.keys(loginConfig).length > 0) || !!(whiteLabel && Object.keys(whiteLabel).length > 0);
    log.setDefaultLevel(logLevel);

    this.consentConfiguration = consentConfiguration;
    this.flowConfig = flowConfig;

    const upbondIframeUrl = new URL(torusUrl);
    if (upbondIframeUrl.pathname.endsWith("/")) upbondIframeUrl.pathname += "popup";
    else upbondIframeUrl.pathname += "/popup";

    upbondIframeUrl.hash = `#isCustomLogin=${this.isCustomLogin}&isRedirect=${isUsingDirect}&dappRedirectUri=${encodeURIComponent(
      `${dappRedirectUri}`
    )}`;

    // Iframe code
    this.upbondIframe = htmlToElement<HTMLIFrameElement>(
      `<iframe
        id="upbondIframe"
        allow=${useWalletConnect ? "camera" : ""}
        class="upbondIframe"
        src="${upbondIframeUrl.href}"
        style="display: none; position: fixed; top: 0; right: 0; width: 100%;
        height: 100%; border: none; border-radius: 0; z-index: ${this.modalZIndex}"
      ></iframe>`
    );

    this.torusAlertContainer = htmlToElement<HTMLDivElement>('<div id="torusAlertContainer"></div>');
    this.torusAlertContainer.style.display = "none";
    this.torusAlertContainer.style.setProperty("z-index", this.alertZIndex.toString());

    const { defaultLanguage = getUserLanguage(), customTranslations = {} } = this.whiteLabel || {};
    const mergedTranslations = deepmerge(configuration.translations, customTranslations);
    const languageTranslations = mergedTranslations[defaultLanguage] || configuration.translations[getUserLanguage()];
    this.embedTranslations = languageTranslations.embed;

    const handleSetup = async () => {
      await documentReady();
      return new Promise((resolve, reject) => {
        this.upbondIframe.onload = async () => {
          // only do this if iframe is not full screen
          await this._setupWeb3();
          const initStream = this.communicationMux.getStream("init_stream") as Substream;
          initStream.on("data", (chunk) => {
            const { name, data, error } = chunk;
            if (name === "init_complete" && data.success) {
              // resolve promise
              this.isInitialized = true;
              this._displayIframe();
              resolve(undefined);
            } else if (error) {
              reject(new Error(error));
            }
          });
          initStream.write({
            name: "init_stream",
            data: {
              enabledVerifiers,
              loginConfig,
              whiteLabel: this.whiteLabel,
              buttonPosition: this.buttonPosition,
              buttonSize: this.buttonSize,
              apiKey: this.apiKey,
              skipTKey,
              network,
              widgetConfig: this.widgetConfig,
              mfaLevel,
              skipDialog,
              selectedVerifier,
              directConfig: {
                enabled: isUsingDirect,
                redirectUrl: dappRedirectUri,
              },
              consentConfiguration: this.consentConfiguration,
              flowConfig,
              state,
            },
          });
        };
        window.document.body.appendChild(this.upbondIframe);
        window.document.body.appendChild(this.torusAlertContainer);
      });
    };

    if (!widgetConfig) {
      log.info(`widgetConfig is not configured. Now using default widget configuration`);
    } else {
      this.widgetConfig = widgetConfig;
    }

    if (buildEnv === "production" && integrity.check) {
      // hacky solution to check for iframe integrity
      const fetchUrl = `${torusUrl}/popup`;
      const resp = await fetch(fetchUrl, { cache: "reload" });
      if (resp.headers.get("Cache-Control") !== expectedCacheControlHeader) {
        throw new Error(`Unexpected Cache-Control headers, got ${resp.headers.get("Cache-Control")}`);
      }
      const response = await resp.text();
      const calculatedIntegrity = generateIntegrity(
        {
          algorithms: ["sha384"],
        },
        response
      );
      if (calculatedIntegrity === integrity.hash) {
        await handleSetup();
      } else {
        this.clearInit();
        throw new Error("Integrity check failed");
      }
    } else {
      try {
        await handleSetup();
        if (this.consentConfiguration.enable && this.consentConfiguration.config.publicKey) {
          this.consent = new Consent({
            publicKey: this.consentConfiguration.config.publicKey,
            scope: this.consentConfiguration.config.scope,
            consentStream: this.communicationMux,
            provider: this.provider,
            isLoggedIn: this.isLoggedIn,
          });
          this.consent.init();
        } else {
          this.consent = null;
        }
      } catch (error) {
        console.error(error, "@errorOnInit");
      }
    }
    return undefined;
  }

  login({ verifier = "", login_hint: loginHint = "" }: TorusLoginParams = {}): Promise<string[]> {
    if (!this.isInitialized) throw new Error("Call init() first");
    this.requestedVerifier = verifier;
    this.loginHint = loginHint;
    return this.ethereum.enable();
  }

  requestAuthServiceAccessToken(): Promise<string> {
    return new Promise((resolve, reject) => {
      const stream = this.communicationMux.getStream("auth_service") as Substream;
      stream.write({
        name: "access_token_request",
      });

      stream.on("data", (ev) => {
        if (ev.name !== "error") {
          resolve(ev.data.authServiceAccessToken);
        } else {
          reject(ev.data.msg);
        }
      });
    });
  }

  logout(): Promise<void> {
    return new Promise((resolve, reject) => {
      if (!this.isInitialized) {
        reject(new Error("Please initialize first"));
        return;
      }

      const logOutStream = this.communicationMux.getStream("logout") as Substream;
      logOutStream.write({ name: "logOut" });
      const statusStream = this.communicationMux.getStream("status") as Substream;
      const statusStreamHandler = (status) => {
        if (!status.loggedIn) {
          this.isLoggedIn = false;
          this.currentVerifier = "";
          this.requestedVerifier = "";
          resolve();
        } else reject(new Error("Some Error Occured"));
      };
      handleStream(statusStream, "data", statusStreamHandler);

      // Remove localstorage upbond_login used for caching
      localStorage.removeItem("upbond_login");
    });
  }

  async cleanUp(): Promise<void> {
    if (this.isLoggedIn) {
      await this.logout();
    }
    this.clearInit();
  }

  clearInit(): void {
    function isElement(element: unknown) {
      return element instanceof Element || element instanceof HTMLDocument;
    }
    if (isElement(this.styleLink) && window.document.body.contains(this.styleLink)) {
      this.styleLink.remove();
      this.styleLink = undefined;
    }
    if (isElement(this.upbondIframe) && window.document.body.contains(this.upbondIframe)) {
      this.upbondIframe.remove();
      this.upbondIframe = undefined;
    }
    if (isElement(this.torusAlertContainer) && window.document.body.contains(this.torusAlertContainer)) {
      this.torusAlert = undefined;
      this.torusAlertContainer.remove();
      this.torusAlertContainer = undefined;
    }
    this.isInitialized = false;
  }

  hideWidget(): void {
    this._sendWidgetVisibilityStatus(false);
    this._displayIframe();
  }

  showWidget(): void {
    this._sendWidgetVisibilityStatus(true);
    this._displayIframe();
  }

  showMenu(): void {
    this._sendWidgetMenuVisibilityStatus(true);
    this._displayIframe(true);
  }

  hideMenu(): void {
    this._sendWidgetMenuVisibilityStatus(false);
    this._displayIframe(false);
  }

  setProvider({ host = "mainnet", chainId = null, networkName = "", ...rest }: NetworkInterface): Promise<void> {
    return new Promise((resolve, reject) => {
      const providerChangeStream = this.communicationMux.getStream("provider_change") as Substream;
      const handler = (chunk) => {
        const { err, success } = chunk.data;
        if (err) {
          reject(err);
        } else if (success) {
          resolve();
        } else reject(new Error("some error occured"));
      };
      handleStream(providerChangeStream, "data", handler);
      const preopenInstanceId = getPreopenInstanceId();
      providerChangeStream.write({
        name: "show_provider_change",
        data: {
          network: {
            host,
            chainId,
            networkName,
            ...rest,
          },
          preopenInstanceId,
          override: false,
        },
      });
    });
  }

  showWallet(path: WALLET_PATH, params: Record<string, string> = {}): void {
    const showWalletStream = this.communicationMux.getStream("show_wallet") as Substream;
    const finalPath = path ? `/${path}` : "";
    showWalletStream.write({ name: "show_wallet", data: { path: finalPath } });

    const showWalletHandler = (chunk) => {
      if (chunk.name === "show_wallet_instance") {
        // Let the error propogate up (hence, no try catch)
        const { instanceId } = chunk.data;
        const finalUrl = new URL(`${this.torusUrl}/wallet${finalPath}`);
        // Using URL constructor to prevent js injection and allow parameter validation.!
        finalUrl.searchParams.append("integrity", "true");
        finalUrl.searchParams.append("instanceId", instanceId);
        Object.keys(params).forEach((x) => {
          finalUrl.searchParams.append(x, params[x]);
        });
        finalUrl.hash = `#isCustomLogin=${this.isCustomLogin}`;
        log.info(`loaded: ${finalUrl}`);
        const walletWindow = new PopupHandler({ url: finalUrl, features: FEATURES_DEFAULT_WALLET_WINDOW });
        walletWindow.open();
      }
    };

    handleStream(showWalletStream, "data", showWalletHandler);
  }

  showSignTransaction(path: WALLET_PATH, params: Record<string, string> = {}): void {
    const showWalletStream = this.communicationMux.getStream("show_wallet") as Substream;
    const finalPath = path ? `/${path}` : "";
    showWalletStream.write({ name: "show_wallet", data: { path: finalPath } });

    const showWalletHandler = (chunk) => {
      if (chunk.name === "show_wallet_instance") {
        // Let the error propogate up (hence, no try catch)
        const { instanceId } = chunk.data;
        const finalUrl = new URL(`${this.torusUrl}/confirm${finalPath}`);
        // Using URL constructor to prevent js injection and allow parameter validation.!
        finalUrl.searchParams.append("integrity", "true");
        finalUrl.searchParams.append("instanceId", instanceId);
        Object.keys(params).forEach((x) => {
          finalUrl.searchParams.append(x, params[x]);
        });
        finalUrl.hash = `#isCustomLogin=${this.isCustomLogin}`;
        log.info(`loaded: ${finalUrl}`);
        const walletWindow = new PopupHandler({ url: finalUrl, features: FEATURES_DEFAULT_WALLET_WINDOW });
        walletWindow.open();
      }
    };

    handleStream(showWalletStream, "data", showWalletHandler);
  }

  async getPublicAddress({ verifier, verifierId, isExtended = false }: VerifierArgs): Promise<string | TorusPublicKey> {
    if (!configuration.supportedVerifierList.includes(verifier) || !WALLET_OPENLOGIN_VERIFIER_MAP[verifier]) throw new Error("Unsupported verifier");
    const walletVerifier = verifier;
    const openloginVerifier = WALLET_OPENLOGIN_VERIFIER_MAP[verifier];
    const url = new URL(`https://api.tor.us/lookup/torus`);
    url.searchParams.append("verifier", openloginVerifier);
    url.searchParams.append("verifierId", verifierId);
    url.searchParams.append("walletVerifier", walletVerifier);
    url.searchParams.append("network", "mainnet");
    url.searchParams.append("isExtended", isExtended.toString());
    return get(
      url.href,
      {
        headers: {
          "Content-Type": "application/json; charset=utf-8",
        },
      },
      { useAPIKey: true }
    );
  }

  getUserInfo(message?: string): Promise<UserInfo> {
    return new Promise((resolve, reject) => {
      if (this.isLoggedIn) {
        const userInfoAccessStream = this.communicationMux.getStream("user_info_access") as Substream;
        userInfoAccessStream.write({ name: "user_info_access_request" });
        const userInfoAccessHandler = (chunk) => {
          const {
            name,
            data: { approved, payload, rejected, newRequest },
          } = chunk;
          if (name === "user_info_access_response") {
            if (approved) {
              resolve(payload);
            } else if (rejected) {
              reject(new Error("User rejected the request"));
            } else if (newRequest) {
              const userInfoStream = this.communicationMux.getStream("user_info") as Substream;
              const userInfoHandler = (handlerChunk) => {
                if (handlerChunk.name === "user_info_response") {
                  if (handlerChunk.data.approved) {
                    resolve(handlerChunk.data.payload);
                  } else {
                    reject(new Error("User rejected the request"));
                  }
                }
              };
              handleStream(userInfoStream, "data", userInfoHandler);
              const preopenInstanceId = getPreopenInstanceId();
              this._handleWindow(preopenInstanceId, {
                target: "_blank",
                features: FEATURES_PROVIDER_CHANGE_WINDOW,
              });
              userInfoStream.write({ name: "user_info_request", data: { message, preopenInstanceId } });
            }
          }
        };
        handleStream(userInfoAccessStream, "data", userInfoAccessHandler);
      } else reject(new Error("User has not logged in yet"));
    });
  }

  getTkey(message?: string) {
    return new Promise((resolve, reject) => {
      if (this.isLoggedIn) {
        const tkeyAccessStream = this.communicationMux.getStream("tkey_access") as Substream;
        tkeyAccessStream.write({ name: "tkey_access_request" });
        const tkeyAccessHandler = (chunk) => {
          const {
            name,
            data: { approved, payload, rejected, newRequest },
          } = chunk;
          if (name === "tkey_access_response") {
            if (approved) {
              resolve(payload);
            } else if (rejected) {
              reject(new Error("User rejected the request"));
            } else if (newRequest) {
              const tkeyInfoStream = this.communicationMux.getStream("tkey") as Substream;
              const tkeyInfoHandler = (handlerChunk) => {
                if (handlerChunk.name === "tkey_response") {
                  if (handlerChunk.data.approved) {
                    resolve(handlerChunk.data.payload);
                  } else {
                    reject(new Error("User rejected the request"));
                  }
                }
              };
              handleStream(tkeyInfoStream, "data", tkeyInfoHandler);
              const preopenInstanceId = getPreopenInstanceId();
              this._handleWindow(preopenInstanceId, {
                target: "_blank",
                features: FEATURES_PROVIDER_CHANGE_WINDOW,
              });
              tkeyInfoStream.write({ name: "tkey_request", data: { message, preopenInstanceId } });
            }
          }
        };
        handleStream(tkeyAccessStream, "data", tkeyAccessHandler);
      } else reject(new Error("User has not logged in yet"));
    });
  }

  getMpcProvider() {
    return new Promise((resolve, reject) => {
      const mpcProviderStream = this.communicationMux.getStream("mpc_provider_access") as Substream;
      mpcProviderStream.write({ name: "mpc_provider_request" });
      const tkeyAccessHandler = (chunk) => {
        const {
          name,
          data: { approved, payload },
        } = chunk;

        this._displayIframe(true);
        if (name === "mpc_provider_response") {
          if (approved) {
            resolve(payload);
          } else {
            reject(new Error("User rejected the request"));
          }
        }
      };
      handleStream(mpcProviderStream, "data", tkeyAccessHandler);
    });
  }

  sendTransaction(data: any) {
    return new Promise((resolve, reject) => {
      this._displayIframe(true);
      const stream = this.communicationMux.getStream("send_transaction_access") as Substream;
      stream.write({ name: "send_transaction_request", data });
      const tkeyAccessHandler = (chunk) => {
        const {
          name,
          data: { approved, payload },
        } = chunk;

        if (name === "send_transaction_response") {
          if (approved) {
            resolve(payload);
          } else {
            reject(new Error("User rejected the request"));
          }
        }
      };
      handleStream(stream, "data", tkeyAccessHandler);
    });
  }

  initiateTopup(provider: PAYMENT_PROVIDER_TYPE, params: PaymentParams): Promise<boolean> {
    return new Promise((resolve, reject) => {
      if (this.isInitialized) {
        const { errors, isValid } = validatePaymentProvider(provider, params);
        if (!isValid) {
          reject(new Error(JSON.stringify(errors)));
          return;
        }
        const topupStream = this.communicationMux.getStream("topup") as Substream;
        const topupHandler = (chunk) => {
          if (chunk.name === "topup_response") {
            if (chunk.data.success) {
              resolve(chunk.data.success);
            } else {
              reject(new Error(chunk.data.error));
            }
          }
        };
        handleStream(topupStream, "data", topupHandler);
        const preopenInstanceId = getPreopenInstanceId();
        this._handleWindow(preopenInstanceId);
        topupStream.write({ name: "topup_request", data: { provider, params, preopenInstanceId } });
      } else reject(new Error("Upbond is not initialized yet"));
    });
  }

  async loginWithPrivateKey(loginParams: { privateKey: string; userInfo: Omit<UserInfo, "isNewUser"> }): Promise<void> {
    const { privateKey, userInfo } = loginParams;
    return new Promise((resolve, reject) => {
      if (this.isInitialized) {
        if (Buffer.from(privateKey, "hex").length !== 32) {
          reject(new Error("Invalid private key, Please provide a 32 byte valid secp25k1 private key"));
          return;
        }
        const loginPrivKeyStream = this.communicationMux.getStream("login_with_private_key") as Substream;
        const loginHandler = (chunk) => {
          if (chunk.name === "login_with_private_key_response") {
            if (chunk.data.success) {
              resolve(chunk.data.success);
            } else {
              reject(new Error(chunk.data.error));
            }
          }
        };
        handleStream(loginPrivKeyStream, "data", loginHandler);
        loginPrivKeyStream.write({ name: "login_with_private_key_request", data: { privateKey, userInfo } });
      } else reject(new Error("Upbond is not initialized yet"));
    });
  }

  async showWalletConnectScanner(): Promise<void> {
    if (!this.useWalletConnect) throw new Error("Set `useWalletConnect` as true in init function options to use wallet connect scanner");
    return new Promise((resolve, reject) => {
      if (this.isLoggedIn) {
        const walletConnectStream = this.communicationMux.getStream("wallet_connect_stream") as Substream;
        const walletConnectHandler = (chunk) => {
          if (chunk.name === "wallet_connect_stream_res") {
            if (chunk.data.success) {
              resolve(chunk.data.success);
            } else {
              reject(new Error(chunk.data.error));
            }
            this._displayIframe();
          }
        };
        handleStream(walletConnectStream, "data", walletConnectHandler);
        walletConnectStream.write({ name: "wallet_connect_stream_req" });
        this._displayIframe(true);
      } else reject(new Error("User has not logged in yet"));
    });
  }

  protected _handleWindow(preopenInstanceId: string, { url, target, features }: { url?: string; target?: string; features?: string } = {}): void {
    if (preopenInstanceId) {
      const windowStream = this.communicationMux.getStream("window") as Substream;
      const finalUrl = new URL(url || `${this.torusUrl}/redirect?preopenInstanceId=${preopenInstanceId}`);
      if (finalUrl.hash) finalUrl.hash += `&isCustomLogin=${this.isCustomLogin}`;
      else finalUrl.hash = `#isCustomLogin=${this.isCustomLogin}`;

      const handledWindow = new PopupHandler({ url: finalUrl, target, features });
      handledWindow.open();
      if (!handledWindow.window) {
        this._createPopupBlockAlert(preopenInstanceId, finalUrl.href);
        return;
      }
      windowStream.write({
        name: "opened_window",
        data: {
          preopenInstanceId,
        },
      });
      const closeHandler = ({ preopenInstanceId: receivedId, close }) => {
        if (receivedId === preopenInstanceId && close) {
          handledWindow.close();
          windowStream.removeListener("data", closeHandler);
        }
      };
      windowStream.on("data", closeHandler);
      handledWindow.once("close", () => {
        windowStream.write({
          data: {
            preopenInstanceId,
            closed: true,
          },
        });
        windowStream.removeListener("data", closeHandler);
      });
    }
  }

  protected _setEmbedWhiteLabel(element: HTMLElement): void {
    // Set whitelabel
    const { theme } = this.whiteLabel || {};
    if (theme) {
      const { isDark = false, colors = {} } = theme;
      if (isDark) element.classList.add("torus-dark");

      if (colors.torusBrand1) element.style.setProperty("--torus-brand-1", colors.torusBrand1);
      if (colors.torusGray2) element.style.setProperty("--torus-gray-2", colors.torusGray2);
    }
  }

  protected _getLogoUrl(): string {
    let logoUrl = `${this.torusUrl}/images/torus_icon-blue.svg`;
    if (this.whiteLabel?.theme?.isDark) {
      logoUrl = this.whiteLabel?.logoLight || logoUrl;
    } else {
      logoUrl = this.whiteLabel?.logoDark || logoUrl;
    }

    return logoUrl;
  }

  protected _sendWidgetVisibilityStatus(status: boolean): void {
    const upbondButtonVisibilityStream = this.communicationMux.getStream("widget-visibility") as Substream;
    upbondButtonVisibilityStream.write({
      data: status,
    });
  }

  protected _sendWidgetMenuVisibilityStatus(status: boolean): void {
    const upbondButtonVisibilityStream = this.communicationMux.getStream("menu-visibility") as Substream;
    upbondButtonVisibilityStream.write({
      data: status,
    });
  }

  protected _displayIframe(isFull = false): void {
    const style: Partial<CSSStyleDeclaration> = {};
    const size = this.buttonSize + 14; // 15px padding
    // set phase
    if (!isFull) {
      style.display = this.isLoggedIn ? "block" : !this.isLoggedIn ? "block" : "none";
      style.height = `${size}px`;
      style.width = `${size}px`;
      switch (this.buttonPosition) {
        case BUTTON_POSITION.TOP_LEFT:
          style.top = "0px";
          style.left = "0px";
          style.right = "auto";
          style.bottom = "auto";
          break;
        case BUTTON_POSITION.TOP_RIGHT:
          style.top = "0px";
          style.right = "0px";
          style.left = "auto";
          style.bottom = "auto";
          break;
        case BUTTON_POSITION.BOTTOM_RIGHT:
          style.bottom = "0px";
          style.right = "0px";
          style.top = "auto";
          style.left = "auto";
          break;
        case BUTTON_POSITION.BOTTOM_LEFT:
        default:
          style.bottom = "0px";
          style.left = "0px";
          style.top = "auto";
          style.right = "auto";
          break;
      }
    } else {
      style.display = "block";
      style.width = "100%";
      style.height = "100%";
      style.top = "0px";
      style.right = "0px";
      style.left = "0px";
      style.bottom = "0px";
    }
    Object.assign(this.upbondIframe.style, style);
    this.isIframeFullScreen = isFull;
  }

  protected async _setupWeb3(): Promise<void> {
    // setup background connection
    const metamaskStream = new BasePostMessageStream({
      name: "embed_metamask",
      target: "iframe_metamask",
      targetWindow: this.upbondIframe.contentWindow,
      targetOrigin: new URL(this.torusUrl).origin,
    });

    // Due to compatibility reasons, we should not set up multiplexing on window.metamaskstream
    // because the MetamaskInpageProvider also attempts to do so.
    // We create another LocalMessageDuplexStream for communication between dapp <> iframe
    const communicationStream = new BasePostMessageStream({
      name: "embed_comm",
      target: "iframe_comm",
      targetWindow: this.upbondIframe.contentWindow,
      targetOrigin: new URL(this.torusUrl).origin,
    });

    // Backward compatibility with Gotchi :)
    // window.metamaskStream = this.communicationStream

    // compose the inpage provider
    const inpageProvider = new UpbondInpageProvider(metamaskStream);

    // detect eth_requestAccounts and pipe to enable for now
    const detectAccountRequestPrototypeModifier = (m) => {
      const originalMethod = inpageProvider[m];
      inpageProvider[m] = function providerFunc(method, ...args) {
        if (method && method === "eth_requestAccounts") {
          return inpageProvider.enable();
        }
        return originalMethod.apply(this, [method, ...args]);
      };
    };

    detectAccountRequestPrototypeModifier("send");
    detectAccountRequestPrototypeModifier("sendAsync");

    inpageProvider.enable = () => {
      return new Promise((resolve, reject) => {
        // If user is already logged in, we assume they have given access to the website
        inpageProvider.sendAsync({ jsonrpc: "2.0", id: getPreopenInstanceId(), method: "eth_requestAccounts", params: [] }, (err, response) => {
          const { result: res } = (response as { result: unknown }) || {};
          if (err) {
            setTimeout(() => {
              reject(err);
            }, 50);
          } else if (Array.isArray(res) && res.length > 0) {
            // If user is already rehydrated, resolve this
            // else wait for something to be written to status stream
            const handleLoginCb = () => {
              if (this.requestedVerifier !== "" && this.currentVerifier !== this.requestedVerifier) {
                const { requestedVerifier } = this;
                // eslint-disable-next-line promise/no-promise-in-callback
                this.logout()
                  // eslint-disable-next-line promise/always-return
                  .then((_) => {
                    this.requestedVerifier = requestedVerifier;
                    this._showLoginPopup(true, resolve, reject);
                  })
                  .catch((error) => reject(error));
              } else {
                resolve(res);
              }
            };
            if (this.isLoggedIn) {
              handleLoginCb();
            } else {
              this.isLoginCallback = handleLoginCb;
            }
          } else {
            // set up listener for login
            this._showLoginPopup(true, resolve, reject, this.skipDialog);
          }
        });
      });
    };

    inpageProvider.tryPreopenHandle = (payload: UnvalidatedJsonRpcRequest | UnvalidatedJsonRpcRequest[], cb: (...args: unknown[]) => void) => {
      const _payload = payload;
      if (this.buildEnv.includes("v1")) {
        if (!Array.isArray(_payload) && UNSAFE_METHODS.includes(_payload.method)) {
          const preopenInstanceId = getPreopenInstanceId();
          this._handleWindow(preopenInstanceId, {
            target: "_blank",
            features: FEATURES_CONFIRM_WINDOW,
          });
          _payload.preopenInstanceId = preopenInstanceId;
        }
      }
      inpageProvider._rpcEngine.handle(_payload as JRPCRequest<unknown>[], cb);
    };

    // Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound
    // `sendAsync` method on the prototype, causing `this` reference issues with drizzle
    const proxiedInpageProvider = new Proxy(inpageProvider, {
      // straight up lie that we deleted the property so that it doesnt
      // throw an error in strict mode
      deleteProperty: () => true,
    });

    this.ethereum = proxiedInpageProvider;
    const communicationMux = setupMultiplex(communicationStream);

    this.communicationMux = communicationMux;

    const windowStream = communicationMux.getStream("window") as Substream;
    windowStream.on("data", (chunk) => {
      if (chunk.name === "create_window") {
        // url is the url we need to open
        // we can pass the final url upfront so that it removes the step of redirecting to /redirect and waiting for finalUrl
        this._createPopupBlockAlert(chunk.data.preopenInstanceId, chunk.data.url);
      }
    });

    // show torus widget if button clicked
    const widgetStream = communicationMux.getStream("widget") as Substream;
    widgetStream.on("data", async (chunk) => {
      const { data } = chunk;
      this._displayIframe(data);
    });

    // Show torus button if wallet has been hydrated/detected
    const statusStream = communicationMux.getStream("status") as Substream;
    statusStream.on("data", (status) => {
      // login
      if (status.loggedIn && localStorage.getItem("upbond_login")) {
        this.isLoggedIn = status.loggedIn;
        this.currentVerifier = status.verifier;
      } // logout
      else this._displayIframe();
      if (this.isLoginCallback) {
        this.isLoginCallback();
        delete this.isLoginCallback;
      }
    });

    this.provider = proxiedInpageProvider;

    if (this.provider.shouldSendMetadata) sendSiteMetadata(this.provider._rpcEngine);
    inpageProvider._initializeState();

    const getCachedData = localStorage.getItem("upbond_login");
    if (window.location.search || getCachedData || this.idToken) {
      let data;
      if (getCachedData) {
        data = JSON.parse(getCachedData) ? JSON.parse(getCachedData) : null;
      }

      if (this.idToken) {
        console.log("@masuk sini?");
        const idTokenParsed = parseIdToken(this.idToken);
        console.log("@idTokenParsed", idTokenParsed);
        if (idTokenParsed?.wallet_address) {
          data = { loggedIn: true, rehydrate: true, selectedAddress: idTokenParsed?.wallet_address, verifier: "" };
          localStorage.setItem("upbond_login", JSON.stringify(data));
        }
      }

      if (window.location.search) {
        const { loggedIn, rehydrate, selectedAddress, verifier, state } = searchToObject<{
          loggedIn: string;
          rehydrate: string;
          selectedAddress: string;
          verifier: string;
          state: string;
        }>(window.location.search);
        if (loggedIn !== undefined && rehydrate !== undefined && selectedAddress !== undefined && verifier !== undefined && state !== undefined) {
          data = { loggedIn, rehydrate, selectedAddress, verifier, state };
          localStorage.setItem("upbond_login", JSON.stringify(data));
        }
      }
      if (data) {
        const oauthStream = this.communicationMux.getStream("oauth") as Substream;
        const isLoggedIn = data.loggedIn === "true";
        const isRehydrate = data.rehydrate === "true";
        let state = "";

        if (data.state) {
          state = data.state;
        }

        const { selectedAddress, verifier } = data;
        if (isLoggedIn) {
          this.isLoggedIn = true;
          this.currentVerifier = verifier;
        } else this._displayIframe(true);

        this._displayIframe(true);

        oauthStream.write({ selectedAddress });
        statusStream.write({ loggedIn: isLoggedIn, rehydrate: isRehydrate, verifier, state });

        await inpageProvider._initializeState();

        if (data.selectedAddress && data.loggedIn && data.state) {
          const urlParams = new URLSearchParams(window.location.search);
          urlParams.delete("selectedAddress");
          urlParams.delete("rehydrate");
          urlParams.delete("loggedIn");
          urlParams.delete("verifier");
          urlParams.delete("state");
          const newQueryParams = urlParams.toString();
          const baseUrl = window.location.href.split("?")[0];
          const newUrl = `${baseUrl}?${newQueryParams}`;
          window.history.replaceState(null, null, newUrl);
        }
      } else {
        const logOutStream = this.communicationMux.getStream("logout") as Substream;
        logOutStream.write({ name: "logOut" });
        const statusStreamHandler = () => {
          this.isLoggedIn = false;
          this.currentVerifier = "";
          this.requestedVerifier = "";
        };
        handleStream(statusStream, "data", statusStreamHandler);
      }
    } else {
      const logOutStream = this.communicationMux.getStream("logout") as Substream;
      logOutStream.write({ name: "logOut" });
      const statusStreamHandler = () => {
        this.isLoggedIn = false;
        this.currentVerifier = "";
        this.requestedVerifier = "";
      };
      handleStream(statusStream, "data", statusStreamHandler);
    }
  }

  protected _showLoginPopup(calledFromEmbed: boolean, resolve: (a: string[]) => void, reject: (err: Error) => void, skipDialog = false): void {
    const loginHandler = async (data) => {
      const { err, selectedAddress } = data;
      if (err) {
        log.error(err);
        if (reject) reject(err);
      }
      // returns an array (cause accounts expects it)
      else if (resolve) resolve([selectedAddress]);
      if (this.isIframeFullScreen) this._displayIframe(false);
    };
    const oauthStream = this.communicationMux.getStream("oauth") as Substream;
    if (!this.requestedVerifier) {
      this._displayIframe(true);
      handleStream(oauthStream, "data", loginHandler);
      oauthStream.write({
        name: "oauth_modal",
        data: {
          calledFromEmbed,
          skipDialog,
          isUsingDirect: this.isUsingDirect,
          verifier: this.currentVerifier,
          dappRedirectUrl: this.dappRedirectUrl,
          selectedVerifier: this.selectedVerifier,
        },
      });
    } else {
      handleStream(oauthStream, "data", loginHandler);
      const preopenInstanceId = getPreopenInstanceId();
      this._handleWindow(preopenInstanceId);
      oauthStream.write({
        name: "oauth",
        data: {
          calledFromEmbed,
          verifier: this.requestedVerifier,
          preopenInstanceId,
          login_hint: this.loginHint,
          skipDialog,
          selectedVerifier: this.selectedVerifier,
        },
      });
    }
  }

  protected _createPopupBlockAlert(preopenInstanceId: string, url: string): void {
    const logoUrl = this._getLogoUrl();
    const torusAlert = htmlToElement<HTMLDivElement>(
      '<div id="torusAlert" class="torus-alert--v2">' +
        `<div id="torusAlert__logo"><img src="${logoUrl}" /></div>` +
        "<div>" +
        `<h1 id="torusAlert__title">${this.embedTranslations.actionRequired}</h1>` +
        `<p id="torusAlert__desc">${this.embedTranslations.pendingAction}</p>` +
        "</div>" +
        "</div>"
    );

    const successAlert = htmlToElement(`<div><a id="torusAlert__btn">${this.embedTranslations.continue}</a></div>`);
    const btnContainer = htmlToElement('<div id="torusAlert__btn-container"></div>');
    btnContainer.appendChild(successAlert);
    torusAlert.appendChild(btnContainer);
    const bindOnLoad = () => {
      successAlert.addEventListener("click", () => {
        this._handleWindow(preopenInstanceId, {
          url,
          target: "_blank",
          features: FEATURES_CONFIRM_WINDOW,
        });
        torusAlert.remove();

        if (this.torusAlertContainer.children.length === 0) this.torusAlertContainer.style.display = "none";
      });
    };

    this._setEmbedWhiteLabel(torusAlert);

    const attachOnLoad = () => {
      this.torusAlertContainer.style.display = "block";
      this.torusAlertContainer.appendChild(torusAlert);
    };

    runOnLoad(attachOnLoad);
    runOnLoad(bindOnLoad);
  }
}

export default Upbond;
