import type { WalletInit, EIP1193Provider } from '@web3-onboard/common'
import type { MagicInitOptions } from './types.js'
import { validateMagicInitOptions } from './validation.js'

function magic(options: MagicInitOptions): WalletInit {
  if (options) {
    const error = validateMagicInitOptions(options)

    if (error) {
      throw error
    }
  }
  const { apiKey, userEmail } = options
  const walletName = 'Magic Wallet'

  return () => {
    return {
      label: walletName,
      getIcon: async () => (await import('./icon.js')).default,
      getInterface: async ({ EventEmitter, BigNumber, chains }) => {
        const { Magic, RPCErrorCode } = await import('magic-sdk')
        const loginModal = (await import('./login-modal.js')).default
        const brandingHTML = (await import('./branding.js')).default

        const {
          createEIP1193Provider,
          ProviderRpcErrorCode,
          ProviderRpcError
        } = await import('@web3-onboard/common')

        const emitter = new EventEmitter()

        if (!chains.length)
          throw new Error(
            'At least one Chain must be passed to onboard in order to connect'
          )

        let currentChain = chains[0]

        let customNodeOptions = {
          chainId: parseInt(currentChain.id),
          rpcUrl: currentChain.rpcUrl
        }

        let magicInstance = new Magic(apiKey, {
          network: customNodeOptions
        })

        let loggedIn: boolean

        const loginWithEmail = async (emailAddress: string) => {
          try {
            await magicInstance.auth.loginWithMagicLink({ email: emailAddress })
            return await magicInstance.user.isLoggedIn()
          } catch (err) {
            throw new Error(
              `An error occurred while connecting your Magic wallet, please try again: ${err}`
            )
          }
        }

        const handleLogin = async () => {
          loggedIn = await loginModal({
            walletName: walletName,
            brandingHTMLString: brandingHTML,
            emailLoginFunction: loginWithEmail
          })
        }

        let magicProvider = magicInstance.rpcProvider
        let provider: EIP1193Provider
        let activeAddress: string

        function patchProvider(): EIP1193Provider {
          const patchedProvider = createEIP1193Provider(magicProvider, {
            eth_requestAccounts: async ({ baseRequest }) => {
              try {
                if (userEmail) loggedIn = await loginWithEmail(userEmail)
                if (!loggedIn) await handleLogin()
                const accounts = await baseRequest({ method: 'eth_accounts' })
                activeAddress = accounts[0]
                return accounts
              } catch (error) {
                const { code } = error as { code: number }
                if (code === RPCErrorCode.InternalError) {
                  throw new ProviderRpcError({
                    code: ProviderRpcErrorCode.ACCOUNT_ACCESS_REJECTED,
                    message: 'Account access rejected'
                  })
                }
                return []
              }
            },
            eth_selectAccounts: null,
            eth_getBalance: async ({ baseRequest }) => {
              const balance = await baseRequest({
                method: 'eth_getBalance',
                params: [activeAddress, 'latest']
              })
              return balance
                ? BigNumber.from(balance).mul('1000000000000000000').toString()
                : '0'
            },
            wallet_switchEthereumChain: async ({ params }) => {
              const chain = chains.find(({ id }) => id === params[0].chainId)
              if (!chain) throw new Error('Chain must be set before switching')
              currentChain = chain
              // re-instantiate instance with new network
              customNodeOptions = {
                chainId: parseInt(currentChain.id),
                rpcUrl: currentChain.rpcUrl
              }

              magicInstance = new Magic(apiKey, {
                network: customNodeOptions
              })

              magicProvider = magicInstance.rpcProvider

              emitter.emit('chainChanged', currentChain.id)

              patchProvider()

              return null
            },
            eth_sign: async ({ params }) => {
              const receipt = await magicProvider.send('eth_sign', params)
              return receipt &&
                receipt.hasOwnProperty('tx') &&
                receipt.tx.hasOwnProperty('hash')
                ? receipt.tx.hash
                : ''
            },
            eth_signTypedData: async ({ params }) => {
              const receipt = await magicProvider.send('eth_sign', params)
              return receipt &&
                receipt.hasOwnProperty('tx') &&
                receipt.tx.hasOwnProperty('hash')
                ? receipt.tx.hash
                : ''
            },
            eth_chainId: async () =>
              currentChain && currentChain.hasOwnProperty('id')
                ? currentChain.id
                : '0x1',

            eth_signTransaction: async ({ params: [transactionObject] }) => {
              let destination
              if (transactionObject.hasOwnProperty('to')) {
                destination = transactionObject.to
              }

              const receipt = await magicProvider.send('eth_signTransaction', [
                {
                  ...transactionObject,
                  nonce:
                    transactionObject.hasOwnProperty('nonce') &&
                    typeof transactionObject.nonce === 'number'
                      ? parseInt(transactionObject.nonce)
                      : '',
                  from: activeAddress,
                  to: destination
                }
              ])
              return receipt &&
                receipt.hasOwnProperty('tx') &&
                receipt.tx.hasOwnProperty('hash')
                ? receipt.tx.hash
                : ''
            }
          })

          if (!provider) {
            patchedProvider.on = emitter.on.bind(emitter)
            patchedProvider.disconnect = () => magicInstance.user.logout()

            return patchedProvider
          } else {
            provider.request = patchedProvider.request.bind(patchedProvider)

            // @ts-ignore - bind old methods for backwards compat
            provider.send = patchedProvider.send.bind(patchedProvider)

            // @ts-ignore - bind old methods for backwards compat
            provider.sendAsync = patchedProvider.sendAsync.bind(patchedProvider)

            return provider
          }
        }

        provider = patchProvider()

        return {
          provider,
          instance: magicInstance
        }
      }
    }
  }
}

export default magic
