import type { ParachainId } from '@crypto-dex-sdk/chain'
import type { Type } from '@crypto-dex-sdk/currency'
import type { QueryableStorageEntry } from '@polkadot/api/types'
import { zenlinkAssetIdToAddress } from '@crypto-dex-sdk/format'
import { JSBI } from '@crypto-dex-sdk/math'
import { useAccount, useApi, useBlockNumber, useCallMulti } from '@crypto-dex-sdk/polkadot'
import { useEffect, useMemo, useState } from 'react'
import { nodePrimitiveCurrencyToZenlinkProtocolPrimitivesAssetId } from '../libs'

import '@pendulum-chain/types/argument/api-rpc'

interface UserReward {
  token: string
  amount: string
}

interface FarmReward {
  pid: number
  nextClaimableBlock?: string
  nextClaimableTime?: string
  userRewards: UserReward[]
}

interface UseFarmsRewardsParams {
  account: string | undefined
  pids: (number | undefined)[]
  chainId?: ParachainId
  enabled?: boolean
}

type UseFarmsRewards = (params: UseFarmsRewardsParams) => {
  data: Record<number, FarmReward>
  isLoading: boolean
  isError: boolean
}

type FarmRewardsMap = Record<number, FarmReward>

export const useFarmsRewards: UseFarmsRewards = ({
  chainId,
  account,
  enabled = true,
  pids = [],
}) => {
  const api = useApi(chainId)
  const { isAccount } = useAccount()
  const [userRewards, setUserRewards] = useState<any[]>([])
  const blockNumber = useBlockNumber()

  const poolInfoCalls: [QueryableStorageEntry<'promise'>, number | undefined][] = useMemo(
    () => api
      ? pids.map(pid => [api.query.farming.poolInfos, pid])
      : [],
    [api, pids],
  )

  const poolInfos = useCallMulti<any[]>({
    chainId,
    calls: poolInfoCalls,
    options: { enabled: enabled && Boolean(api) },
  })

  const poolInfoMap = useMemo(
    () => poolInfos.reduce((map, info, i) => {
      const pid = pids[i]
      const poolInfo = info?.isSome
        ? info?.value?.toJSON()
        : undefined

      if (!pid || !poolInfo)
        return map
      map[pid] = info
      return map
    }, {}),
    [pids, poolInfos],
  )

  const userShareInfo = useCallMulti<any[]>({
    chainId,
    calls: (api && isAccount(account))
      ? pids
          .map(pid => [api.query.farming.sharesAndWithdrawnRewards, [pid, account]])
          .filter((call): call is [QueryableStorageEntry<'promise'>, [number, string]] => Boolean(call[0]))
      : [],
    options: { enabled: enabled && Boolean(api && isAccount(account)) },
  })

  const userShareInfoMap = useMemo(() => {
    const result: { [pid: number]: any } = {}
    if (userShareInfo.length !== pids.length)
      return result
    for (let i = 0; i < pids.length; i++) {
      const pid = pids[i]
      const value = userShareInfo[i]?.value?.toJSON()
      if (value && pid)
        result[pid] = value
    }
    return result
  }, [userShareInfo, pids])

  useEffect(() => {
    if (!api || !account || !isAccount(account))
      return

    if (!api.rpc.farming.getFarmingRewards || !api.rpc.farming.getGaugeRewards)
      return

    try {
      Promise.all(
        pids.map((pid) => {
          return Promise.all([
            api.rpc.farming.getFarmingRewards(account, Number(pid)),
            api.rpc.farming.getGaugeRewards(account, Number(pid)),
          ])
        }),
      ).then((result) => {
        const userReward = result.map((reward, index) => {
          const pid = pids[index]
          const [farmingRewards, gaugeRewards] = reward

          const userRewards = Object.entries([...farmingRewards, ...gaugeRewards]
            .map((item) => {
              const token = nodePrimitiveCurrencyToZenlinkProtocolPrimitivesAssetId(
                item[0].toHuman() as any,
                chainId as number,
              )

              return {
                token: zenlinkAssetIdToAddress(token),
                amount: item[1].toString(),
              }
            })
            .reduce<Record<string, { token: string, amount: string }>>((map, cur) => {
              if (!map[cur.token]) {
                map[cur.token] = {
                  token: cur.token,
                  amount: cur.amount,
                }
              }
              else {
                map[cur.token].amount = JSBI.add(
                  JSBI.BigInt(cur.amount),
                  JSBI.BigInt(map[cur.token].amount),
                ).toString()
              }
              return map
            }, {})).map(item => item[1])

          return { pid, userRewards }
        })

        setUserRewards(userReward)
      })
    }
    catch {}
  }, [account, api, chainId, isAccount, pids, blockNumber])

  const userFarmInfosMap: FarmRewardsMap = useMemo(() => {
    const result: FarmRewardsMap = {}

    if (userRewards.length !== pids.length)
      return result

    for (let i = 0; i < pids.length; i++) {
      const value = userRewards[i]
      const poolInfo = poolInfoMap[pids[i]!]?.poolInfo
      const claimLimitTime = poolInfo?.claimLimitTime ?? 0
      const userShareInfo = userShareInfoMap[pids[i]!]
      const claimLastBlock = userShareInfo?.claimLastBlock ?? 0

      if (value)
        value.nextClaimableBlock = claimLastBlock + claimLimitTime
      result[pids[i]!] = value
    }
    return result
  }, [pids, poolInfoMap, userRewards, userShareInfoMap])

  return useMemo(() => ({
    data: userFarmInfosMap,
    isLoading: isAccount(account) && !userRewards.length && !!pids.length,
    isError: !isAccount(account),
  }), [userFarmInfosMap, isAccount, account, userRewards.length, pids.length])
}

interface UseFarmRewardsParams {
  account: string | undefined
  currency: Type | undefined
  chainId?: ParachainId
  pid: number
  enabled?: boolean
}

type UseFarmRewards = (params: UseFarmRewardsParams) => Pick<ReturnType<typeof useFarmsRewards>, 'isError' | 'isLoading'> & {
  data?: FarmReward
}

export const useFarmRewards: UseFarmRewards = ({
  chainId,
  account,
  enabled,
  pid,
}) => {
  const pids = useMemo(() => [pid], [pid])
  const { data, isLoading, isError } = useFarmsRewards({ chainId, pids, account, enabled })

  return useMemo(() => {
    const balance = pid
      ? data?.[pid]
      : undefined

    return {
      isError,
      isLoading,
      data: balance,
    }
  }, [data, isError, isLoading, pid])
}
