import { BigNumber } from 'ethers';
import { omit } from 'lodash';
import { SignatureLike } from '@ethersproject/bytes';
import { verifyMessage, verifyTypedData } from 'ethers/lib/utils';
import { v4 as uuidv4 } from 'uuid';
import { Transaction, VersionedTransaction } from '@solana/web3.js';
import { IDomain, ITypes, IMessage } from './defination';

const clearLoopObserver = (
  clientWindow: Window | null,
  handleMessage: (e: any) => void,
) => {
  const interval = setInterval(function () {
    if (!clientWindow || clientWindow.closed) {
      clearInterval(interval);
      window.removeEventListener('message', handleMessage);
    }
  }, 500);
  return interval;
};

const checkWindowOpenStatus = (
  clientWindow: Window | null,
  reject: (reason?: any) => void,
  msg: any = 'The pop-up cannot be ejected',
) => {
  const id = setTimeout(() => {
    clearTimeout(id);
    if (!clientWindow) {
      reject(msg);
    }
  }, 2000);
};

const envPromiseCheckWrapper = (
  callback: (
    res: (value: any | PromiseLike<any>) => void,
    rej: (reason?: any) => void,
  ) => any,
  clientWindow: Window | null,
  msg?: any,
) => {
  return new Promise((resolve, reject) => {
    checkWindowOpenStatus(clientWindow, reject, msg);
    callback(resolve, reject);
  });
};

interface FSLLoginOptions {
  responseType?: string;

  appKey: string;

  redirectUri?: string;

  scope?: string;

  state?: string;

  usePopup?: boolean;

  isApp?: boolean;

  domain?: string;
}

class FSLAuthorization {
  responseType?: string;
  appKey: string;
  redirectUri?: string;
  scope?: string;
  state?: string;
  usePopup?: boolean;
  domain?: string;
  isApp?: boolean;
  windowFeatures = `left=${window.screen.width / 2 - 200},top=${
    window.screen.height / 2 - 500
  },width=500,height=800,popup=1`;
  private constructor(opt: FSLLoginOptions) {
    const {
      responseType,
      appKey,
      redirectUri,
      scope,
      state,
      usePopup,
      domain,
      isApp,
    } = opt;
    this.appKey = appKey;
    this.responseType = responseType;
    this.redirectUri = redirectUri;
    this.scope = scope;
    this.usePopup = usePopup;
    this.state = state;
    this.domain = domain;
    this.isApp = isApp;
  }

  static init(opt: FSLLoginOptions) {
    return new FSLAuthorization(opt);
  }

  async signIn(args?: { withState: boolean }) {
    const callUrl = new URL(
      `${this.domain || 'https://id.fsl.com'}/login/fslUsers`,
    );
    const commonArgs: Record<string, string | undefined> = {
      response_type: this.responseType,
      appkey: this.appKey,
      scope: this.scope,
      state: this.state,
      is_app: this.isApp ? '1' : undefined,
      withState: args?.withState ? '1' : undefined,
    };
    for (let key in commonArgs) {
      if (commonArgs[key]) {
        callUrl.searchParams.append(key, commonArgs[key]!);
      }
    }

    if (!this.usePopup) {
      callUrl.searchParams.append('redirect_uri', this.redirectUri!);
      location.href = callUrl.toString();
      return Promise.resolve(null);
    } else {
      callUrl.searchParams.append('use_popup', '1');
      if (this.isApp) {
        callUrl.searchParams.append('redirect_uri', this.redirectUri!);
      }
      const clientWindow = window.open(
        callUrl.toString(),
        this.isApp ? '_blank' : `signWindow`,
        this.windowFeatures,
      );
      if (this.isApp) {
        return Promise.resolve(null);
      }
      return envPromiseCheckWrapper((resolve) => {
        const handleMessage = (e: any) => {
          if (e.data.type === 'fsl_login') {
            resolve(e.data.data);
            window.removeEventListener('message', handleMessage);
          }
        };
        window.addEventListener('message', handleMessage, false);
        clearLoopObserver(clientWindow, handleMessage);
      }, clientWindow);
    }
  }

  async signInV2() {
    return this.signIn({ withState: true });
  }

  static evmVerifyMessage(msg: string, signature: SignatureLike) {
    return verifyMessage(msg, signature);
  }

  static evmVerifyTypedData(
    domain: IDomain,
    types: ITypes,
    message: IMessage,
    signature: SignatureLike,
  ) {
    return verifyTypedData(domain, types, message, signature);
  }

  async callEvmSign(args: {
    msg: any;
    rpc?: string;
    chainId: number;
    chain?: string;
    domain?: string;
    uid?: number;
    signDigest?: boolean;
  }) {
    const { msg, chainId, chain, rpc, signDigest } = args;
    const callUrl = new URL(
      `${
        args.domain || this.domain || 'https://id.fsl.com'
      }/authorization/sign`,
    );
    let type: string;
    switch (true) {
      case msg instanceof Uint8Array:
        type = 'unit8Array';
        break;
      case msg instanceof Uint16Array:
        type = 'unit16Array';
        break;
      case msg instanceof Uint32Array:
        type = 'unit32Array';
        break;
      default:
        type = '';
    }

    const uuid = uuidv4();

    callUrl.searchParams.append(
      'arguments',
      encodeURIComponent(
        JSON.stringify({
          id: uuid,
          appKey: this.appKey,
          rpc,
          chainId,
          chain,
        }),
      ),
    );

    if (args.uid) {
      callUrl.searchParams.append('uid', args.uid + '');
    }
    const url = callUrl.toString();

    const clientWindow = window.open(url, `evmSignWindow`, this.windowFeatures);
    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            if (e.data.data === 'done') {
              clientWindow &&
                clientWindow.postMessage(
                  {
                    type: 'fsl_params',
                    data: JSON.stringify({
                      id: uuid,
                      msg,
                      type,
                      signDigest: signDigest ? 1 : undefined,
                    }),
                  },
                  '*',
                );
              return;
            } else {
              resolve(e.data.data);
            }
          } else {
            reject(e.data.data);
          }
          window.removeEventListener('message', handleMessage);
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  async callEvmSignDigest(args: {
    msg: any;
    rpc?: string;
    chainId: number;
    chain?: string;
    domain?: string;
    uid?: number;
  }) {
    return this.callEvmSign({ ...args, signDigest: true });
  }

  async signTransaction(args: {
    contractAddress: string;
    methodName: string;
    abi?: any;
    chainId: number;
    chain?: string;
    value?: string;
    gasLimit: string;
    params?: any[];
    to?: string;
    rpc?: string;
    domain?: string;
    nonce?: number;
    maxPriorityFeePerGasValue?: BigNumber;
    maxFeePerGasValue?: BigNumber;
    uid?: number;
  }) {
    const callUrl = new URL(
      `${
        args.domain || this.domain || 'https://id.fsl.com'
      }/authorization/trade`,
    );

    callUrl.searchParams.append(
      'arguments',
      JSON.stringify({
        ...omit(args, 'domain', 'uid'),
        onlySign: 'onlySign',
        appKey: this.appKey,
      }),
    );

    if (args.uid) {
      callUrl.searchParams.append('uid', args.uid + '');
    }
    const url = callUrl.toString();

    const clientWindow = window.open(
      url,
      `signEvmContractWindow`,
      this.windowFeatures,
    );

    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            resolve(e.data.data);
          } else {
            reject(e.data.data);
          }
          window.removeEventListener('message', handleMessage);
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  async callEvmContract(args: {
    contractAddress: string;
    methodName: string;
    abi?: any;
    chainId: number;
    chain?: string;
    value?: string;
    gasLimit: string;
    params?: any[];
    to?: string;
    rpc?: string;
    domain?: string;
    nonce?: number;
    maxPriorityFeePerGasValue?: BigNumber;
    maxFeePerGasValue?: BigNumber;
    uid?: number;
    confirmed?: boolean;
  }) {
    if (window.callContractWindow && !window.callContractWindow.closed) {
      if (window.callContractHandler) {
        window.removeEventListener('message', window.callContractHandler);
        window.callContractHandler = null;
      }
      if (window.callContractInterval) {
        clearInterval(window.callContractInterval);
        window.callContractInterval = void 0;
      }
      window.callContractWindow.close();
      await new Promise((resolve) =>
        setTimeout(() => {
          resolve(true);
        }, 1000),
      );
    }

    const callUrl = new URL(
      `${
        args.domain || this.domain || 'https://id.fsl.com'
      }/authorization/trade`,
    );
    args.confirmed = args.confirmed || false;
    callUrl.searchParams.append(
      'arguments',
      JSON.stringify({
        ...omit(args, 'domain', 'uid'),
        appKey: this.appKey,
      }),
    );

    if (args.uid) {
      callUrl.searchParams.append('uid', args.uid + '');
    }
    const url = callUrl.toString();
    window.callContractWindow = window.open(
      url,
      `evmContractWindow`,
      this.windowFeatures,
    );
    return envPromiseCheckWrapper((resolve, reject) => {
      window.callContractHandler = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (
            e.data.data &&
            typeof e.data.data === 'object' &&
            'transactionHash' in e.data.data
          ) {
            resolve(e.data.data);
          } else {
            reject(e.data.data);
          }
          window.callContractHandler &&
            window.removeEventListener('message', window.callContractHandler);
        }
      };
      window.addEventListener('message', window.callContractHandler, false);
      window.callContractInterval = clearLoopObserver(
        window.callContractWindow,
        window.callContractHandler,
      );
    }, window.callContractWindow);
  }

  async signTypedData(args: {
    domain: IDomain;
    types: ITypes;
    message: IMessage;
    chainId: number;
    mockDomain?: string;
    chain?: string;
    uid?: number;
  }) {
    const { domain, types, message, mockDomain, chain, chainId, uid } = args;
    const uuid = uuidv4();

    const callUrl = new URL(
      `${
        mockDomain || this.domain || 'https://id.fsl.com'
      }/authorization/sign-v4`,
    );

    callUrl.searchParams.append(
      'arguments',
      encodeURIComponent(
        JSON.stringify({
          id: uuid,
        }),
      ),
    );

    if (uid) {
      callUrl.searchParams.append('uid', uid + '');
    }
    const url = callUrl.toString();

    const clientWindow = window.open(
      url,
      `typedSignWindow`,
      this.windowFeatures,
    );
    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            if (e.data.data === 'done') {
              clientWindow &&
                clientWindow.postMessage(
                  {
                    type: 'fsl_params',
                    data: JSON.stringify({
                      message,
                      chain,
                      chainId,
                      types,
                      domain,
                      id: uuid,
                      appKey: this.appKey,
                    }),
                  },
                  '*',
                );
            } else {
              resolve(e.data.data);
              window.removeEventListener('message', handleMessage);
            }
          } else {
            reject(e.data.data);
            window.removeEventListener('message', handleMessage);
          }
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  async callEvmContractByCallData(args: {
    contractAddress: string;
    callData: string;
    chainId: number;
    gasLimit: string;
    value?: string;
    chain?: string;
    rpc?: string;
    domain?: string;
    nonce?: number;
    onlySign?: boolean;
    maxPriorityFeePerGasValue?: BigNumber;
    maxFeePerGasValue?: BigNumber;
    uid?: number;
    confirmed?: boolean;
  }) {
    const uuid = uuidv4();
    const {
      contractAddress,
      callData,
      chainId,
      gasLimit,
      value,
      chain,
      rpc,
      domain,
      nonce,
      onlySign,
      maxPriorityFeePerGasValue,
      maxFeePerGasValue,
      uid,
      confirmed = false,
    } = args;

    const callUrl = new URL(
      `${
        domain || this.domain || 'https://id.fsl.com'
      }/authorization/call-data`,
    );

    callUrl.searchParams.append(
      'arguments',
      JSON.stringify({
        id: uuid,
        onlySign: onlySign ? 'onlySign' : undefined,
      }),
    );

    if (uid) {
      callUrl.searchParams.append('uid', uid + '');
    }
    const url = callUrl.toString();

    const clientWindow = window.open(
      url,
      `callDataWindow`,
      this.windowFeatures,
    );
    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            if (e.data.data === 'done') {
              clientWindow &&
                clientWindow.postMessage(
                  {
                    type: 'fsl_params',
                    data: JSON.stringify({
                      contractAddress,
                      callData,
                      chainId,
                      gasLimit,
                      value,
                      chain,
                      rpc,
                      nonce,
                      maxFeePerGasValue,
                      maxPriorityFeePerGasValue,
                      id: uuid,
                      confirmed,
                      appKey: this.appKey,
                    }),
                  },
                  '*',
                );
            } else {
              resolve(e.data.data);
              window.removeEventListener('message', handleMessage);
            }
          } else if (
            typeof e.data.data === 'object' &&
            'transactionHash' in e.data.data
          ) {
            resolve(e.data.data);
            window.removeEventListener('message', handleMessage);
          } else {
            reject(e.data.data);
            window.removeEventListener('message', handleMessage);
          }
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  async signCallDataTransaction(args: {
    contractAddress: string;
    callData: string;
    chainId: number;
    gasLimit: string;
    value?: string;
    chain?: string;
    rpc?: string;
    domain?: string;
    nonce?: number;
    maxPriorityFeePerGasValue?: BigNumber;
    maxFeePerGasValue?: BigNumber;
    uid?: number;
  }) {
    return this.callEvmContractByCallData({ ...args, onlySign: true });
  }

  signSolMessage(args: { msg: string; domain?: string; uid?: number }) {
    const { msg, domain, uid } = args;
    const callUrl = new URL(
      `${domain || this.domain || 'https://id.fsl.com'}/authorization/sol-sign`,
    );
    callUrl.searchParams.append(
      'arguments',
      encodeURIComponent(
        JSON.stringify({
          msg,
          appKey: this.appKey,
        }),
      ),
    );

    if (uid) {
      callUrl.searchParams.append('uid', uid + '');
    }

    const url = callUrl.toString();
    const clientWindow = window.open(
      url,
      `signSolMsgWindow`,
      this.windowFeatures,
    );

    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (
            e.data.data &&
            typeof e.data.data === 'object' &&
            'length' in e.data.data
          ) {
            resolve(e.data.data);
          } else {
            reject(e.data.data);
          }
          window.removeEventListener('message', handleMessage);
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  callSolInstructions(args: {
    instructions: any[];
    keypairs: any[];
    rpc?: string;
    unitLimit?: number;
    unitPrice?: number;
    domain?: string;
    onlySign?: boolean;
    uid?: number;
  }) {
    const {
      domain,
      instructions,
      keypairs,
      rpc,
      unitPrice,
      unitLimit,
      onlySign,
      uid,
    } = args;
    const uuid = uuidv4();

    const callUrl = new URL(
      `${
        domain || this.domain || 'https://id.fsl.com'
      }/authorization/sol-trade`,
    );
    callUrl.searchParams.append(
      'arguments',
      JSON.stringify({
        appKey: this.appKey,
        rpc,
        onlySign: onlySign ? 'onlySign' : void 0,
        id: uuid,
      }),
    );

    if (uid) {
      callUrl.searchParams.append('uid', uid + '');
    }

    const url = callUrl.toString();
    const clientWindow = window.open(
      url,
      `signSolCallWindow`,
      this.windowFeatures,
    );

    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = async (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            if (e.data.data === 'done') {
              clientWindow &&
                clientWindow.postMessage(
                  {
                    type: 'fsl_params',
                    data: JSON.stringify({
                      instructions,
                      keypairs,
                      unitLimit,
                      unitPrice,
                      id: uuid,
                    }),
                  },
                  '*',
                );
            } else {
              resolve(e.data.data);
              window.removeEventListener('message', handleMessage);
            }
          } else {
            reject(e.data.data);
            window.removeEventListener('message', handleMessage);
          }
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  signSolInstructions(args: {
    instructions: any[];
    keypairs: any[];
    rpc?: string;
    unitLimit?: number;
    unitPrice?: number;
    domain?: string;
    uid?: number;
  }) {
    return this.callSolInstructions({ ...args, onlySign: true });
  }

  signSolTransaction(args: { transactions: any; uid?: number }) {
    const { transactions, uid } = args;
    let bufferStrs: string[] = [];
    const versions: Array<'legacy' | 0> = [];

    try {
      bufferStrs = transactions.map((item: any) => {
        if (item.version === 0) {
          versions.push(0);
          return Buffer.from(
            item.serialize({ verifySignatures: false }),
          ).toString('base64');
        } else {
          versions.push('legacy');
          return item.serialize({ verifySignatures: false }).toString('base64');
        }
      });
    } catch (err: any) {
      return Promise.reject(err.message);
    }

    const uuid = uuidv4();

    const callUrl = new URL(
      `${this.domain || 'https://id.fsl.com'}/authorization/sol-transaction`,
    );
    callUrl.searchParams.append(
      'arguments',
      JSON.stringify({
        id: uuid,
      }),
    );

    if (uid) {
      callUrl.searchParams.append('uid', uid + '');
    }

    const url = callUrl.toString();
    const clientWindow = window.open(
      url,
      `signSolTrsWindow`,
      this.windowFeatures,
    );

    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = async (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            if (e.data.data === 'done') {
              clientWindow &&
                clientWindow.postMessage(
                  {
                    type: 'fsl_params',
                    data: JSON.stringify({
                      appKey: this.appKey,
                      transactions: bufferStrs,
                      versions,
                      id: uuid,
                    }),
                  },
                  '*',
                );
            }
          } else if (Array.isArray(e.data.data)) {
            const handledBufferStrs = e.data.data;
            const transactions: Array<VersionedTransaction | Transaction> = [];
            try {
              for (let i = 0; i < versions.length; i++) {
                if (versions[i] === 0) {
                  const newTransaction = VersionedTransaction.deserialize(
                    Buffer.from(handledBufferStrs[i], 'base64'),
                  );
                  transactions.push(newTransaction);
                } else {
                  const newTransaction = Transaction.from(
                    Buffer.from(handledBufferStrs[i], 'base64'),
                  );
                  transactions.push(newTransaction);
                }
              }
            } catch (err: any) {
              console.log(err.message);
            }
            resolve(transactions);
            window.removeEventListener('message', handleMessage);
          } else {
            reject(e.data.data);
            window.removeEventListener('message', handleMessage);
          }
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }

  async callEvmContractV2(args: {
    contractAddress: string;
    methodName: string;
    abi?: any;
    chainId: number;
    chain?: string;
    value?: string;
    gasLimit: string;
    params?: any[];
    to?: string;
    rpc?: string;
    domain?: string;
    nonce?: number;
    maxPriorityFeePerGasValue?: BigNumber;
    maxFeePerGasValue?: BigNumber;
    onlySign?: boolean;
    uid?: number;
  }) {
    const {
      contractAddress,
      methodName,
      abi,
      chainId,
      chain,
      value,
      gasLimit,
      params,
      to,
      rpc,
      domain,
      nonce,
      maxPriorityFeePerGasValue,
      maxFeePerGasValue,
      onlySign,
      uid,
    } = args;
    const id = uuidv4();

    const callUrl = new URL(
      `${domain || this.domain || 'https://id.fsl.com'}/authorization/trade-v2`,
    );
    callUrl.searchParams.append(
      'arguments',
      JSON.stringify({
        appKey: this.appKey,
        onlySign: onlySign ? 'onlySign' : undefined,
        chainId,
        chain,
        rpc,
        id,
      }),
    );

    if (uid) {
      callUrl.searchParams.append('uid', uid + '');
    }

    const url = callUrl.toString();

    const clientWindow = window.open(
      url,
      `evmContractWindow`,
      this.windowFeatures,
    );
    return envPromiseCheckWrapper((resolve, reject) => {
      const handleMessage = (e: any) => {
        if (e.data.type === 'fsl_auth') {
          if (typeof e.data.data === 'string') {
            if (e.data.data === 'done') {
              clientWindow &&
                clientWindow.postMessage(
                  {
                    type: 'fsl_params',
                    data: JSON.stringify({
                      contractAddress,
                      methodName,
                      abi,
                      value,
                      gasLimit,
                      params,
                      to,
                      nonce,
                      maxPriorityFeePerGasValue,
                      maxFeePerGasValue,
                      id,
                    }),
                  },
                  '*',
                );
            } else {
              resolve(e.data.data);
              window.removeEventListener('message', handleMessage);
            }
          } else if (
            e.data.data &&
            typeof e.data.data === 'object' &&
            'transactionHash' in e.data.data
          ) {
            resolve(e.data.data);
            window.removeEventListener('message', handleMessage);
          } else {
            reject(e.data.data);
            window.removeEventListener('message', handleMessage);
          }
        }
      };
      window.addEventListener('message', handleMessage, false);
      clearLoopObserver(clientWindow, handleMessage);
    }, clientWindow);
  }
  async signEvmContractV2(args: {
    contractAddress: string;
    methodName: string;
    abi?: any;
    chainId: number;
    chain?: string;
    value?: string;
    gasLimit: string;
    params?: any[];
    to?: string;
    rpc?: string;
    domain?: string;
    nonce?: number;
    maxPriorityFeePerGasValue?: BigNumber;
    maxFeePerGasValue?: BigNumber;
    uid?: number;
  }) {
    return this.callEvmContractV2({ ...args, onlySign: true });
  }
}

export default FSLAuthorization;
