import type { Currency, Token, Type } from '@crypto-dex-sdk/currency'

import type { QueryableStorageEntry } from '@polkadot/api/types'
import type { FrameSystemAccountInfo } from '@polkadot/types/lookup'
import type { OrmlTokensAccountData } from '@zenlink-types/bifrost/interfaces'
import type { PairPrimitivesAssetId } from '../types'
import { Pair } from '@crypto-dex-sdk/amm'
import { ParachainId } from '@crypto-dex-sdk/chain'
import { Amount } from '@crypto-dex-sdk/currency'
import { addressToZenlinkAssetId } from '@crypto-dex-sdk/format'
import { useApi, useCallMulti } from '@crypto-dex-sdk/polkadot'

import { useMemo } from 'react'
import { addressToNodeCurrency, isNativeCurrency, PAIR_ADDRESSES } from '../libs'
// Explicitly import the @polkadot/api-augment here, to re-apply the base types,
// see https://polkadot.js.org/docs/api/FAQ/#since-upgrading-to-the-7x-series-typescript-augmentation-is-missing
import '@polkadot/api-augment'

export enum PairState {
  LOADING,
  NOT_EXISTS,
  EXISTS,
  INVALID,
}

export function getPairs(chainId: number | undefined, currencies: [Currency | undefined, Currency | undefined][]) {
  return currencies
    .filter((currencies): currencies is [Type, Type] => {
      const [currencyA, currencyB] = currencies
      return Boolean(
        chainId
        && (chainId === ParachainId.AMPLITUDE || chainId === ParachainId.PENDULUM)
        && currencyA
        && currencyB
        && currencyA.chainId === currencyB.chainId
        && chainId === currencyA.chainId
        && !currencyA.wrapped.equals(currencyB.wrapped),
      )
    })
    .reduce<[Token[], Token[], PairPrimitivesAssetId[]]>(
      (acc, [currencyA, currencyB]) => {
        const [token0, token1] = currencyA.wrapped.sortsBefore(currencyB.wrapped)
          ? [currencyA.wrapped, currencyB.wrapped]
          : [currencyB.wrapped, currencyA.wrapped]

        acc[0].push(token0)
        acc[1].push(token1)
        acc[2].push([
          addressToZenlinkAssetId(token0.address),
          addressToZenlinkAssetId(token1.address),
        ])
        return acc
      },
      [[], [], []],
    )
}

export function uniqePairKey(tokenA: Token, tokenB: Token): string {
  return `${tokenA.address}-${tokenB.address}`
}

interface UsePairsReturn {
  isLoading: boolean
  isError: boolean
  data: [PairState, Pair | null][]
}

export function usePairs(
  chainId: number | undefined,
  currencies: [Currency | undefined, Currency | undefined][],
  enabled = true,
): UsePairsReturn {
  const api = useApi(chainId)
  const [tokensA, tokensB] = useMemo(() => getPairs(chainId, currencies), [chainId, currencies])

  const [validTokensA, validTokensB, reservesCalls] = useMemo(
    () => tokensA.reduce<[Token[], Token[], [QueryableStorageEntry<'promise'>, ...unknown[]][]]>(
      (acc, tokenA, i) => {
        const tokenB = tokensB[i]
        const pairKey = uniqePairKey(tokenA, tokenB)
        const pairAccount = PAIR_ADDRESSES[pairKey]?.account
        if (pairAccount && api) {
          acc[0].push(tokenA)
          acc[1].push(tokenB)
          if (isNativeCurrency(tokenA))
            acc[2].push([api.query.system.account, pairAccount])
          else
            acc[2].push([api.query.tokens.accounts, [pairAccount, addressToNodeCurrency(tokenA.address)]])

          if (isNativeCurrency(tokenB))
            acc[2].push([api.query.system.account, pairAccount])
          else
            acc[2].push([api.query.tokens.accounts, [pairAccount, addressToNodeCurrency(tokenB.address)]])
        }
        return acc
      },
      [[], [], []],
    ),
    [api, tokensA, tokensB],
  )

  const reserves = useCallMulti<(OrmlTokensAccountData | FrameSystemAccountInfo)[]>({
    chainId,
    calls: reservesCalls,
    options: { defaultValue: [], enabled: enabled && !!api },
  })

  return useMemo(() => {
    if (!reservesCalls.length)
      return { isLoading: true, isError: false, data: [[PairState.NOT_EXISTS, null]] }
    if (!reserves.length || reserves.length !== validTokensA.length * 2) {
      return {
        isLoading: true,
        isError: false,
        data: validTokensA.map(() => [PairState.LOADING, null]),
      }
    }

    return {
      isLoading: Boolean(reserves.length),
      isError: false,
      data: validTokensA.map((tokenA, i) => {
        const tokenB = validTokensB[i]
        if (!tokenA || !tokenB || tokenA.equals(tokenB))
          return [PairState.INVALID, null]

        const pairKey = uniqePairKey(tokenA, tokenB)
        const reserve0 = reserves[i * 2]
        const reserve1 = reserves[i * 2 + 1]
        const pairAddress = PAIR_ADDRESSES[pairKey]?.address
        if (!reserve0 || !reserve1 || reserve0.isEmpty || reserve1.isEmpty || !pairAddress)
          return [PairState.NOT_EXISTS, null]

        return [
          PairState.EXISTS,
          new Pair(
            Amount.fromRawAmount(
              tokenA,
              ((reserve0 as FrameSystemAccountInfo).data || reserve0).free.toString(),
            ),
            Amount.fromRawAmount(
              tokenB,
              ((reserve1 as FrameSystemAccountInfo).data || reserve1).free.toString(),
            ),
            pairAddress,
          ),
        ]
      }),
    }
  }, [reserves, reservesCalls.length, validTokensA, validTokensB])
}

interface UsePairReturn {
  isLoading: boolean
  isError: boolean
  data: [PairState, Pair | null]
}

export function usePair(
  chainId: number,
  tokenA?: Currency,
  tokenB?: Currency,
  enabled?: boolean,
): UsePairReturn {
  const inputs: [[Currency | undefined, Currency | undefined]] = useMemo(() => [[tokenA, tokenB]], [tokenA, tokenB])
  const { data, isLoading, isError } = usePairs(chainId, inputs, enabled)

  return useMemo(
    () => ({
      isLoading,
      isError,
      data: data?.[0],
    }),
    [data, isError, isLoading],
  )
}
