import { ApolloClient, InMemoryCache, gql } from '@apollo/client/core'

import {
  _META,
  GET_DEPOSITS,
  GET_STATISTIC,
  GET_REGISTERED,
  GET_WITHDRAWALS,
  GET_NOTE_ACCOUNTS,
  GET_ENCRYPTED_NOTES
} from './queries'

const isEmptyArray = (arr) => !Array.isArray(arr) || !arr.length

const first = 1000

const link = ({ getContext }) => {
  const { chainId } = getContext()
  return CHAIN_GRAPH_URLS[chainId]
}

const CHAIN_GRAPH_URLS = {
  1: 'https://api.thegraph.com/subgraphs/name/tornadocash/mainnet-tornado-subgraph',
  5: 'https://api.thegraph.com/subgraphs/name/tornadocash/goerli-tornado-subgraph',
  10: 'https://api.thegraph.com/subgraphs/name/tornadocash/optimism-tornado-subgraph',
  56: 'https://api.thegraph.com/subgraphs/name/tornadocash/bsc-tornado-subgraph',
  100: 'https://api.thegraph.com/subgraphs/name/tornadocash/xdai-tornado-subgraph',
  137: 'https://api.thegraph.com/subgraphs/name/tornadocash/matic-tornado-subgraph',
  42161: 'https://api.thegraph.com/subgraphs/name/tornadocash/arbitrum-tornado-subgraph',
  43114: 'https://api.thegraph.com/subgraphs/name/tornadocash/avalanche-tornado-subgraph'
}

const defaultOptions = {
  query: {
    fetchPolicy: 'no-cache',
    errorPolicy: 'all'
  }
}

const client = new ApolloClient({
  uri: link,
  cache: new InMemoryCache(),
  defaultOptions
})

const registryClient = new ApolloClient({
  uri: 'https://api.thegraph.com/subgraphs/name/tornadocash/tornado-relayer-registry',
  cache: new InMemoryCache(),
  defaultOptions
})

async function getStatistic({ currency, amount, netId }) {
  try {
    const { data } = await client.query({
      context: {
        chainId: netId
      },
      query: gql(GET_STATISTIC),
      variables: {
        currency,
        first: 10,
        orderBy: 'index',
        orderDirection: 'desc',
        amount: String(amount)
      }
    })

    if (!data) {
      return {
        lastSyncBlock: '',
        events: []
      }
    }

    const { deposits } = data

    const lastSyncBlock = await getMeta({ netId })

    const events = deposits
      .map((e) => ({
        timestamp: e.timestamp,
        leafIndex: Number(e.index),
        blockNumber: Number(e.blockNumber)
      }))
      .reverse()

    const [lastEvent] = events.slice(-1)

    return {
      lastSyncBlock: lastEvent?.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
      events
    }
  } catch {
    return {
      lastSyncBlock: '',
      events: []
    }
  }
}

async function getAllRegisters(fromBlock) {
  try {
    const relayers = await getRegisters(fromBlock)

    if (!relayers) {
      return { lastSyncBlock: '', events: [] }
    }

    const lastSyncBlock = await getRegisteredMeta()

    return { lastSyncBlock, events: relayers }
  } catch {
    return { lastSyncBlock: '', events: [] }
  }
}
async function getAllDeposits({ currency, amount, fromBlock, netId }) {
  try {
    let deposits = []

    while (true) {
      let result = await getDeposits({ currency, amount, fromBlock, netId })

      if (isEmptyArray(result)) {
        break
      }

      if (result.length < 900) {
        deposits = deposits.concat(result)
        break
      }

      const [lastEvent] = result.slice(-1)

      result = result.filter((e) => e.blockNumber !== lastEvent.blockNumber)
      fromBlock = Number(lastEvent.blockNumber)

      deposits = deposits.concat(result)
    }

    if (!deposits) {
      return {
        lastSyncBlock: '',
        events: []
      }
    }

    const lastSyncBlock = await getMeta({ netId })

    const data = deposits.map((e) => ({
      ...e,
      leafIndex: Number(e.index),
      blockNumber: Number(e.blockNumber)
    }))

    const [lastEvent] = data.slice(-1)

    return {
      events: data,
      lastSyncBlock: lastEvent?.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock
    }
  } catch {
    return {
      lastSyncBlock: '',
      events: []
    }
  }
}

async function getMeta({ netId }) {
  try {
    const { data } = await client.query({
      context: {
        chainId: netId
      },
      query: gql(_META)
    })

    if (!data) {
      return undefined
    }

    return data._meta.block.number
  } catch {
    return undefined
  }
}

async function getRegisteredMeta() {
  try {
    const { data } = await registryClient.query({
      context: {
        chainId: 1
      },
      query: gql(_META)
    })

    if (!data) {
      return undefined
    }

    return data._meta.block.number
  } catch {
    return undefined
  }
}

async function getRegisters(fromBlock) {
  const { data } = await registryClient.query({
    context: {
      chainId: 1
    },
    query: gql(GET_REGISTERED),
    variables: { first, fromBlock }
  })

  if (!data) {
    return []
  }

  return data.relayers
}

async function getDeposits({ currency, amount, fromBlock, netId }) {
  const { data } = await client.query({
    context: {
      chainId: netId
    },
    query: gql(GET_DEPOSITS),
    variables: { currency, amount: String(amount), first, fromBlock }
  })

  if (!data) {
    return []
  }

  return data.deposits
}

async function getAllWithdrawals({ currency, amount, fromBlock, netId }) {
  try {
    let withdrawals = []

    while (true) {
      let result = await getWithdrawals({ currency, amount, fromBlock, netId })

      if (isEmptyArray(result)) {
        break
      }

      if (result.length < 900) {
        withdrawals = withdrawals.concat(result)
        break
      }

      const [lastEvent] = result.slice(-1)

      result = result.filter((e) => e.blockNumber !== lastEvent.blockNumber)
      fromBlock = Number(lastEvent.blockNumber)

      withdrawals = withdrawals.concat(result)
    }

    if (!withdrawals) {
      return {
        lastSyncBlock: '',
        events: []
      }
    }

    const lastSyncBlock = await getMeta({ netId })

    const data = withdrawals.map((e) => ({
      ...e,
      nullifierHash: e.nullifier,
      leafIndex: Number(e.index),
      blockNumber: Number(e.blockNumber)
    }))

    const [lastEvent] = data.slice(-1)

    return {
      events: data,
      lastSyncBlock: lastEvent?.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock
    }
  } catch {
    return {
      lastSyncBlock: '',
      events: []
    }
  }
}

async function getWithdrawals({ currency, amount, fromBlock, netId }) {
  const { data } = await client.query({
    context: {
      chainId: netId
    },
    query: gql(GET_WITHDRAWALS),
    variables: { currency, amount: String(amount), fromBlock, first }
  })

  if (!data) {
    return []
  }

  return data.withdrawals
}

async function getNoteAccounts({ address, netId }) {
  try {
    const { data } = await client.query({
      context: {
        chainId: netId
      },
      query: gql(GET_NOTE_ACCOUNTS),
      variables: { address }
    })

    if (!data) {
      return {
        lastSyncBlock: '',
        events: []
      }
    }

    const lastSyncBlock = await getMeta({ netId })

    return {
      lastSyncBlock,
      events: data.noteAccounts
    }
  } catch {
    return {
      lastSyncBlock: '',
      events: []
    }
  }
}

async function getAllEncryptedNotes({ fromBlock, netId }) {
  try {
    let encryptedNotes = []

    while (true) {
      let result = await getEncryptedNotes({ fromBlock, netId })

      if (isEmptyArray(result)) {
        break
      }

      if (result.length < 900) {
        encryptedNotes = encryptedNotes.concat(result)
        break
      }

      const [lastEvent] = result.slice(-1)

      result = result.filter((e) => e.blockNumber !== lastEvent.blockNumber)
      fromBlock = Number(lastEvent.blockNumber)

      encryptedNotes = encryptedNotes.concat(result)

      if (isEmptyArray(result)) {
        break
      }
    }

    if (!encryptedNotes) {
      return {
        lastSyncBlock: '',
        events: []
      }
    }

    const lastSyncBlock = await getMeta({ netId })

    const data = encryptedNotes.map((e) => ({
      txHash: e.transactionHash,
      encryptedNote: e.encryptedNote,
      transactionHash: e.transactionHash,
      blockNumber: Number(e.blockNumber)
    }))

    const [lastEvent] = data.slice(-1)

    return {
      events: data,
      lastSyncBlock: lastEvent?.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock
    }
  } catch {
    return {
      lastSyncBlock: '',
      events: []
    }
  }
}

async function getEncryptedNotes({ fromBlock, netId }) {
  const { data } = await client.query({
    context: {
      chainId: netId
    },
    query: gql(GET_ENCRYPTED_NOTES),
    variables: { fromBlock, first }
  })

  if (!data) {
    return []
  }

  return data.encryptedNotes
}

export default {
  getDeposits,
  getStatistic,
  getAllDeposits,
  getWithdrawals,
  getNoteAccounts,
  getAllRegisters,
  getAllWithdrawals,
  getAllEncryptedNotes
}