import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react'

import { useWallet as useSolanaAdapterReactWallet } from '@solana/wallet-adapter-react'
import { OnboardAPI, WalletState } from '@web3-onboard/core'
import { ethers } from 'ethers'

import { instantiateOnboard } from '../../core'
import {
  ChainType,
  Chains,
  PolkadotExtensionState,
  ProviderConfigs,
  SolanaWalletAdapterState
} from '../../core/interfaces'
import { usePolkadotConnector } from '../../store/polkadot-connector/hook'
import { useSolanaConnector } from '../../store/solana-connector/hook'
import { WalletContext, WalletContextProps, useChains } from '../../store/wallet'
import {
  clearConnectedWalletCache,
  constructPolkadotApi,
  getConnectedWalletCache,
  getPolkadotExtensionStateByExtensionLabel,
  getSolanaWalletAdapterStateByAdapterName,
  setConnectedWalletCache,
  switchEvmChain,
  userCanceledError
} from '../../utils'
import { PolkadotConnector } from '../PolkadotConnector/PolkadotConnector'
import { PolkadotConnectorProvider } from '../PolkadotConnector/PolkadotConnectorProvider'
import { SolanaConnector } from '../SolanaConnector/SolanaConnector'
import { SolanaConnectorProvider } from '../SolanaConnector/SolanaConnectorProvider'
import { ChainProvider } from './ChainProvider'

interface WalletProviderProps {
  appName?: string
  prefixCls?: string
  theme?: 'light' | 'dark'
  providerConfigs: ProviderConfigs
  chains: Chains
}

export const WalletProvider: React.FC<PropsWithChildren<WalletProviderProps>> = (props) => {
  const { children, chains, providerConfigs, ...restProps } = props

  return (
    <ChainProvider chains={chains}>
      <SolanaConnectorProvider providerConfigs={providerConfigs}>
        <PolkadotConnectorProvider>
          <WalletProviderImpl providerConfigs={providerConfigs} {...restProps}>
            {children}
            <PolkadotConnector />
            <SolanaConnector />
          </WalletProviderImpl>
        </PolkadotConnectorProvider>
      </SolanaConnectorProvider>
    </ChainProvider>
  )
}

interface WalletProviderImplProps {
  appName?: string
  prefixCls?: string
  theme?: 'light' | 'dark'
  providerConfigs: ProviderConfigs
}

const WalletProviderImpl: React.FC<PropsWithChildren<WalletProviderImplProps>> = ({
  children,
  appName,
  prefixCls = 'ata-',
  theme,
  providerConfigs
}) => {
  const { chainId, setChainId, chains } = useChains()
  const onboardAPI = useRef<OnboardAPI | undefined>()
  const polkadotConnector = usePolkadotConnector()
  const solanaConnector = useSolanaConnector()
  const _solanaWallet = useSolanaAdapterReactWallet()
  const {
    wallets: solanaAdapterReactWallets,
    select: solanaAdapterReactSelect,
    connect: solanaAdapterReactConnect,
    disconnect: solanaAdapterReactDisconnect
  } = _solanaWallet
  const solanaWallet = useMemo(() => {
    const chain = chains.find((item) => item.chainId === chainId)
    if (chain && chain.type === ChainType.Solana) {
      return _solanaWallet
    }
  }, [chainId, chains, _solanaWallet])
  const [account, setAccount] = useState<WalletContextProps['account']>()
  const [ethersProvider, setEthersProvider] = useState<WalletContextProps['ethersProvider']>()
  const [polkadotApi, setPolkadotApi] = useState<WalletContextProps['polkadotApi']>()
  const [walletState, setWalletState] = useState<WalletContextProps['walletState']>()
  const [loading, setLoading] = useState<WalletContextProps['loading']>(true)
  const [reconnected, setReconnected] = useState<boolean>(false)

  const connectPolkadotApi = useCallback(
    async (chainId) => {
      const api = await constructPolkadotApi({ chainId, chains })

      if (api) {
        await api.connect()
      }

      setPolkadotApi(api)
    },
    [chains, setPolkadotApi]
  )

  const clearState = useCallback(() => {
    onboardAPI.current = undefined

    setAccount(undefined)
    setChainId(undefined)
    setEthersProvider(undefined)
    setPolkadotApi(undefined)
    setWalletState(undefined)
  }, [setAccount, setChainId, setEthersProvider, setPolkadotApi, setWalletState])

  const clearEVMState = useCallback(() => {
    onboardAPI.current = undefined

    setEthersProvider(undefined)
  }, [setEthersProvider])

  const clearPolkadotState = useCallback(() => {
    setPolkadotApi(undefined)
  }, [setPolkadotApi])

  async function recoverState(options: {
    chainId: string
    wallets?: WalletState[]
    polkadotExtensionState?: PolkadotExtensionState
    solanaWalletAdapterState?: SolanaWalletAdapterState
  }) {
    const { chainId, wallets, polkadotExtensionState, solanaWalletAdapterState } = options

    if (wallets) {
      const firstWallet = wallets[0]

      if (firstWallet) {
        const { label, icon, provider } = firstWallet

        setConnectedWalletCache({ providerLabel: label, chainId })

        const ethersProvider = new ethers.providers.Web3Provider(provider)

        await ethersProvider.send('eth_requestAccounts', [])

        const chain = chains.find((item) => item.chainId === chainId)

        if (chain) {
          await switchEvmChain({ ethersProvider, chain })
        }

        const accounts = await ethersProvider.listAccounts()
        const account = accounts && accounts.length ? accounts[0] : undefined

        setAccount(account)
        setChainId(chainId)
        setEthersProvider(ethersProvider)
        setWalletState({
          label,
          icon,
          evmProvider: provider,
          accounts
        })
      }
    } else if (polkadotExtensionState) {
      const { label, icon, injected, accounts } = polkadotExtensionState

      setConnectedWalletCache({ providerLabel: label, chainId })

      const account = accounts && accounts.length ? accounts[0] : undefined

      await connectPolkadotApi(chainId)

      setAccount(account)
      setChainId(chainId)
      setWalletState({
        label,
        icon,
        polkadotInjected: injected,
        accounts
      })
    } else if (solanaWalletAdapterState) {
      const { label, icon, adapter, accounts } = solanaWalletAdapterState

      setConnectedWalletCache({ providerLabel: label, chainId })

      const account = accounts && accounts.length ? accounts[0] : undefined

      setAccount(account)
      setChainId(chainId)
      setWalletState({
        label,
        icon,
        solanaWalletAdapter: adapter,
        accounts
      })
    }
  }

  const reconnect = useCallback(async () => {
    try {
      const connectedWalletCache = getConnectedWalletCache()

      if (connectedWalletCache != null) {
        const chain = chains.find((item) => item.chainId === connectedWalletCache.chainId)

        if (!chain) {
          return
        }

        setLoading(true)

        const { type, chainId } = chain

        if (type === ChainType.EVM) {
          if (!onboardAPI.current) {
            onboardAPI.current = onboardAPI.current || instantiateOnboard(providerConfigs, chains)
          }

          if (onboardAPI.current) {
            const wallets = await onboardAPI.current.connectWallet({
              autoSelect: { label: connectedWalletCache.providerLabel, disableModals: true }
            })

            await recoverState({ chainId: chainId, wallets })
          }
        } else if (type === ChainType.Polkadot) {
          // need to wait until the extension inject in to browser
          await new Promise<void>((resolve) => {
            setTimeout(() => {
              resolve()
            }, 1 * 500)
          })

          const polkadotExtensionState = await getPolkadotExtensionStateByExtensionLabel({
            appName,
            label: connectedWalletCache.providerLabel,
            providerConfigs
          })

          await recoverState({ chainId, polkadotExtensionState })
        } else if (type === ChainType.Solana) {
          const solanaWalletAdapterState = await getSolanaWalletAdapterStateByAdapterName({
            name: connectedWalletCache.providerLabel,
            wallets: solanaAdapterReactWallets,
            select: solanaAdapterReactSelect,
            connect: solanaAdapterReactConnect
          })

          await recoverState({ chainId, solanaWalletAdapterState })
        }
      }
    } finally {
      setLoading(false)
    }
  }, [
    solanaAdapterReactWallets,
    solanaAdapterReactSelect,
    solanaAdapterReactConnect,
    providerConfigs,
    chains,
    appName
  ])

  const disconnectEVM = useCallback(async () => {
    if (onboardAPI.current) {
      const [primaryWallet] = onboardAPI.current.state.get().wallets

      if (primaryWallet) {
        await onboardAPI.current.disconnectWallet({ label: primaryWallet.label })
      }
    }
  }, [])

  const disconnectPolkadot = useCallback(async () => {
    if (polkadotApi != null) {
      await polkadotApi.disconnect()
    }
  }, [polkadotApi])

  const disconnectSolana = useCallback(async () => {
    await solanaAdapterReactDisconnect()
  }, [solanaAdapterReactDisconnect])

  const disconnect = useCallback(async () => {
    const chain = chains.find((item) => item.chainId === chainId)

    if (!chain) {
      return
    }

    const { type } = chain

    if (type === ChainType.EVM) {
      await disconnectEVM()
    } else if (type === ChainType.Polkadot) {
      await disconnectPolkadot()
    } else if (type === ChainType.Solana) {
      await disconnectSolana()
    }

    clearState()
    clearConnectedWalletCache()
  }, [clearState, disconnectEVM, disconnectPolkadot, chains, chainId])

  const switchChain = async (chainId: string) => {
    const chain = chains.find((item) => item.chainId === chainId)

    if (!chain) {
      throw new Error('Chain is not existed')
    }

    const { type } = chain

    if (type === ChainType.EVM) {
      if (!ethersProvider || polkadotApi || walletState?.solanaWalletAdapter) {
        await connect(chainId)
        await disconnectPolkadot()
        await disconnectSolana()

        clearPolkadotState()

        return
      }

      await switchEvmChain({ ethersProvider, chain })

      setChainId(chainId)
    } else if (type === ChainType.Polkadot) {
      if (!polkadotApi || ethersProvider || walletState?.solanaWalletAdapter) {
        await connect(chainId)
        await disconnectEVM()
        await disconnectSolana()

        clearEVMState()

        return
      }

      await connectPolkadotApi(chainId)

      setChainId(chainId)
    } else if (type === ChainType.Solana) {
      if (!walletState || !walletState.solanaWalletAdapter || ethersProvider || polkadotApi) {
        await connect(chainId)
        await disconnectEVM()
        await disconnectPolkadot()

        clearEVMState()
        clearPolkadotState()

        return
      }

      setChainId(chainId)
    }
  }

  const connect = useCallback(
    async (chainId: string) => {
      // eslint-disable-next-line no-useless-catch
      const chain = chains.find((item) => item.chainId === chainId)

      if (!chain) {
        throw new Error('Chain is not existed')
      }

      setLoading(true)

      const { type } = chain

      try {
        if (type === ChainType.EVM) {
          if (!onboardAPI.current) {
            onboardAPI.current = instantiateOnboard(providerConfigs, chains)
          }

          if (onboardAPI.current) {
            const wallets = await onboardAPI.current.connectWallet()

            if (wallets.length === 0) {
              throw userCanceledError
            }

            await recoverState({ chainId, wallets })
          }
        } else if (type === ChainType.Polkadot) {
          const extensionState = await polkadotConnector.connect(providerConfigs)

          await recoverState({ chainId, polkadotExtensionState: extensionState })
        } else if (type === ChainType.Solana) {
          const walletAdapterState = await solanaConnector.connect()

          await recoverState({ chainId, solanaWalletAdapterState: walletAdapterState })
        }
      } finally {
        setLoading(false)
      }
    },
    [setLoading, providerConfigs, chains, polkadotConnector]
  )

  useEffect(() => {
    if (!reconnected) {
      setReconnected(true)

      reconnect()
    }
  }, [reconnected, reconnect])

  useEffect(() => {
    function onChainChanged(chainId: number | string) {
      setChainId(`${chainId}`)
    }

    function onAccountsChanged(accounts: string[]) {
      if (!accounts[0]) {
        return disconnect()
      }

      setAccount(accounts[0])
    }

    if (ethersProvider) {
      ethersProvider.on('chainChanged', onChainChanged)
      ethersProvider.on('accountsChanged', onAccountsChanged)
    }

    return () => {
      if (ethersProvider) {
        ethersProvider.off('chainChanged', onChainChanged)
        ethersProvider.off('accountsChanged', onAccountsChanged)
      }
    }
  }, [account, disconnect])

  return (
    <WalletContext.Provider
      value={{
        appName,
        account,
        chainId,
        chains,
        ethersProvider,
        polkadotApi,
        solanaWallet,
        walletState,
        loading,
        prefixCls,
        theme,
        connect,
        disconnect,
        switchChain
      }}
    >
      {children}
    </WalletContext.Provider>
  )
}
