import {
  type AuthEvents,
  type ConnectOptions,
  type Handler,
  type Provider,
  type SignedTx,
  type SignerTx,
  type TypedData,
  type UserData,
} from '@waves/signer';
import mitt from 'mitt';

import { keeperTxFactory, signerTxFactory } from './adapter';

export class ProviderKeeper implements Provider {
  public user: UserData | null = null;
  protected _apiPromise: Promise<WavesKeeper.TWavesKeeperApi>;
  protected _connectPromise: Promise<void>; // used in _ensureApi
  private _connectResolve!: () => void; // initialized in Promise constructor
  private _options: ConnectOptions = {
    NETWORK_BYTE: 'W'.charCodeAt(0),
    NODE_URL: 'https://nodes.wavesnodes.com',
  };
  private readonly _emitter = mitt<AuthEvents>();

  constructor() {
    this._apiPromise = isKeeperInstalled().then(isInstalled => {
      return isInstalled
        ? window.WavesKeeper.initialPromise.then(api => Promise.resolve(api))
        : Promise.reject(new Error('WavesKeeper is not installed.'));
    });

    this._apiPromise.catch(() => {
      // avoid unhandled rejection
    });

    this._connectPromise = new Promise(resolve => {
      this._connectResolve = resolve;
    });
  }

  public on<EVENT extends keyof AuthEvents>(
    event: EVENT,
    handler: Handler<AuthEvents[EVENT]>,
  ): Provider {
    this._emitter.on(event, handler);

    return this;
  }

  public once<EVENT extends keyof AuthEvents>(
    event: EVENT,
    handler: Handler<AuthEvents[EVENT]>,
  ): Provider {
    const wrappedHandler: Handler<AuthEvents[EVENT]> = (...args) => {
      handler(...args);
      this._emitter.off(event, wrappedHandler);
    };

    this._emitter.on(event, wrappedHandler);

    return this;
  }

  public off<EVENT extends keyof AuthEvents>(
    event: EVENT,
    handler: Handler<AuthEvents[EVENT]>,
  ): Provider {
    this._emitter.off(event, handler);

    return this;
  }

  public connect(options: ConnectOptions): Promise<void> {
    this._options = options;
    this._connectResolve();
    return Promise.resolve();
  }

  public login(): Promise<UserData> {
    return this._ensureApi()
      .then(api => api.publicState())
      .then(state => {
        // in this case we already have state.account,
        // otherwise api.publicState will throw an error
        this.user = {
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          address: state.account!.address,
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          publicKey: state.account!.publicKey,
        };
        this._emitter.emit('login', this.user);
        return this.user;
      });
  }

  public logout(): Promise<void> {
    this.user = null;
    this._emitter.emit('logout', void 0);
    return Promise.resolve();
  }

  public signMessage(data: string | number): Promise<string> {
    return this._ensureApi()
      .then(api =>
        api.signCustomData({
          version: 1,
          binary: `base64:${btoa(String(data))}`,
        }),
      )
      .then(result => result.signature);
  }

  public signTypedData(data: TypedData[]): Promise<string> {
    return this._ensureApi()
      .then(api =>
        api.signCustomData({
          version: 2,
          data: data as WavesKeeper.TTypedData[],
        }),
      )
      .then(result => result.signature);
  }

  sign<T extends SignerTx>(toSign: T[]): Promise<SignedTx<T>>;
  sign<T extends SignerTx[]>(toSign: T): Promise<SignedTx<T>>;
  public async sign<T extends SignerTx>(toSign: T[]): Promise<SignedTx<T>> {
    const apiPromise = this._ensureApi();

    if (toSign.length === 1) {
      return apiPromise
        .then(api => api.signTransaction(keeperTxFactory(toSign[0])))
        .then(data => [signerTxFactory(data)]) as Promise<SignedTx<T>>;
    }

    return apiPromise
      .then(api =>
        api.signTransactionPackage(
          toSign.map(tx =>
            keeperTxFactory(tx),
          ) as WavesKeeper.TSignTransactionPackageData,
        ),
      )
      .then(data => data.map(tx => signerTxFactory(tx))) as Promise<
      SignedTx<T>
    >;
  }

  private async _ensureApi(): Promise<WavesKeeper.TWavesKeeperApi> {
    // api is ready
    const [api] = await Promise.all([this._apiPromise, this._connectPromise]);

    // has accounts
    const state = await api.publicState();

    // and is on the same network
    const keeperNetworkByte = state.network.code.charCodeAt(0);
    const signerNetworkByte = this._options.NETWORK_BYTE;

    if (keeperNetworkByte !== signerNetworkByte) {
      const keeperNodeUrl = state.network.server;
      const signerNodeUrl = this._options.NODE_URL;

      throw new Error(
        `Invalid connect options. Signer connect ` +
          `(${signerNodeUrl} ${signerNetworkByte}) not equals ` +
          `keeper connect (${keeperNodeUrl} ${keeperNetworkByte})`,
      );
    }

    return api;
  }
}

const poll = (
  resolve: (result: boolean) => void,
  reject: (...args: unknown[]) => void,
  attempt = 0,
  retries = 30,
  interval = 100,
) => {
  if (attempt > retries) return resolve(false);

  if (typeof WavesKeeper !== 'undefined') {
    return resolve(true);
  } else setTimeout(() => poll(resolve, reject, ++attempt), interval);
};

const _isKeeperInstalled = new Promise(poll);

export async function isKeeperInstalled() {
  return _isKeeperInstalled;
}
