import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useErrorHandler } from 'react-error-boundary';
import uiKeyring from '@polkadot/ui-keyring';
import {
  web3Enable,
  web3FromAddress,
  web3Accounts,
} from '@polkadot/extension-dapp';
import { Polymesh, Keyring } from '@polymathnetwork/polymesh-sdk';
import { ClaimData, PolkadotSigner } from '@polymathnetwork/polymesh-sdk/types';
import { LoadingScreen } from '@polymathnetwork/polymesh-ui';
import orderBy from 'lodash.orderby';
import { getChainProperties } from '../utils/getChainProperties';
import { keyLabel } from '../utils/keyLabelStorage';
import { NetworkName } from '../data/constants';
import { AppStaticRoute, AppBaseReduxConfig, ApiConfig } from './definition';

const requiredReduxActions = [
  'setSdk',
  'setSelectedAccount',
  'setDid',
  'setHasUid',
  'setIsITN',
  'setPolyWallet',
  'setChainProperties',
];

const delay = async (ms: number) =>
  new Promise((resolve) => setTimeout(resolve, ms));

let network: string;
let polyWallet: any;

export interface ApiProps {
  children: React.ReactNode;
  redux: AppBaseReduxConfig;
  appName: string;
  apiConfig: ApiConfig;
}

export const Api = ({ children, redux, appName, apiConfig }: ApiProps) => {
  const [isReady, setIsReady] = useState(false);
  const handleError = useErrorHandler();
  const history = useHistory();
  const { appStateActions, store } = redux;
  const { dispatch } = store;
  const {
    polyWssNodeUrl,
    graphqlEndpoint,
    graphqlApiKey,
    masqueradeEnv,
    isProdEnv,
    rpcUrl,
  } = apiConfig;

  useEffect(() => {
    const { search } = window.location;
    const params = new URLSearchParams(search);
    const masqueradeQuery = params.get('masq');
    const masquerade = masqueradeQuery || masqueradeEnv;

    async function connect(
      address: string,
      keyring: Keyring,
      signer?: PolkadotSigner
    ) {
      dispatch(appStateActions.setSelectedAccount(address));

      const sdk = await Polymesh.connect({
        nodeUrl: polyWssNodeUrl,
        signer,
        keyring,
        middleware: {
          link: graphqlEndpoint,
          key: graphqlApiKey,
        },
      });
      dispatch(appStateActions.setSdk(sdk));

      setIsReady(true);

      try {
        const identity = await sdk.getCurrentIdentity();

        if (identity) {
          const hasUid = await polyWallet.uid.isSet();
          dispatch(appStateActions.setHasUid(hasUid));

          if (network !== NetworkName.itn && !hasUid) {
            history.push(AppStaticRoute.uIdMissing, {
              polyWallet,
              did: identity.did,
              network,
            });
          }

          const didIsValid = await sdk.isIdentityValid({
            identity: identity.did,
          });
          const cddAttestations: ClaimData[] = await sdk.claims.getCddClaims({
            target: identity,
          });
          const cddAttestation = orderBy(
            cddAttestations,
            ['expiry'],
            ['desc']
          )[0];
          dispatch(
            appStateActions.setDid({
              identity,
              didIsValid,
              issuer: cddAttestation?.issuer,
              expiry: cddAttestation?.expiry || undefined,
            })
          );
        } else {
          dispatch(
            appStateActions.setDid({
              didIsValid: false,
            })
          );
        }
      } catch (error) {
        dispatch(
          appStateActions.setDid({
            didIsValid: false,
          })
        );
      }
    }

    async function init() {
      try {
        await delay(500);
        const extensions = await web3Enable(appName);

        if (!masquerade && (!extensions || extensions.length === 0)) {
          history.push(AppStaticRoute.extension);
        }

        polyWallet = (extensions.filter(
          (ext: any) => ext.name === 'polywallet'
        )[0] as unknown) as any;

        if (polyWallet) {
          dispatch(appStateActions.setPolyWallet(polyWallet));

          const networkMeta = await polyWallet.network.get();
          const cleanEndpoint = networkMeta.wssUrl.replace(/\/$/, '');
          const cleanUrl = polyWssNodeUrl.replace(/\/$/, '');

          if (cleanUrl !== cleanEndpoint) {
            history.push(AppStaticRoute.wrongNetwork);
          }

          network = networkMeta.name;
          dispatch(appStateActions.setIsITN(network === NetworkName.itn));

          if (window.location.hostname !== 'localhost') {
            polyWallet.network.subscribe(() => {
              window.location.reload();
            });
            polyWallet.accounts.subscribe(() => {
              window.location.reload();
            });
          }
        } else if (!masquerade) {
          history.push(AppStaticRoute.extension);
        }

        const chainProperties = await getChainProperties(rpcUrl);
        dispatch(appStateActions.setChainProperties(chainProperties as any));

        const { ss58Format } = chainProperties;

        const keyring = new Keyring({ type: 'ed25519', ss58Format });
        let address;
        let signer;

        if (masquerade && !isProdEnv) {
          try {
            uiKeyring.loadAll({
              type: 'ed25519',
              isDevelopment: true,
            });
          } catch (error) {
            // This try-catch is needed to enable use of browser navigation buttons.
            // You can't initialize `keyring` more than once, and using an `if` statement
            // broke things because it couldn't find the test account `alice`.
          }

          const allTestingPairs = uiKeyring.getPairs();

          const testingAccountPair = allTestingPairs.filter(
            ({ meta: { name, isTesting } }: any) =>
              name === masquerade && isTesting === true
          )[0];

          keyring.addPair(testingAccountPair);

          if (testingAccountPair) {
            address = testingAccountPair.address;
            connect(address, keyring, signer);
          } else {
            throw new Error(`Couldn't find test account ${masquerade}`);
          }
        } else {
          const accounts = (await web3Accounts({ ss58Format })).filter(
            (account: any) => account.meta.source === 'polywallet'
          );
          if (accounts[0]) {
            const account = accounts?.[0];
            if (!account) throw new Error('Account not found');
            address = account.address;
            if (account.meta.name) keyLabel.save(address, account.meta.name);
            keyring.addFromAddress(address);
            const injector = await web3FromAddress(address);
            signer = injector.signer;
            connect(address, keyring, signer);
          }
        }
      } catch (error) {
        // Display error via ErrorBoundary fallback component.
        handleError(error);
      }
    }

    const providedReduxActions = Object.keys(appStateActions);
    const missingReduxActions = requiredReduxActions.filter(
      (a) => !providedReduxActions.includes(a)
    );
    if (missingReduxActions.length) {
      throw new Error(
        `Missing Redux actions for API to init: ${missingReduxActions.join(
          ', '
        )}`
      );
    } else {
      init();
    }
  }, []); // eslint-disable-line

  if (isReady) return children;
  else return <LoadingScreen />;
};
