import fetch from 'node-fetch'
// import axios from 'axios'
import store from '@/store'
import compareVersions from 'compare-versions'
import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'
import { fromHex, toBase64 } from '@cosmjs/encoding'
import {
  Proposal, ProposalTally, Proposer, StakingPool, Votes, Deposit,
  Validator, StakingParameters, Block, ValidatorDistribution, StakingDelegation, WrapStdTx, getUserCurrency,
} from './utils'
import OsmosAPI from './osmos'

function commonProcess(res) {
  if (res && Object.keys(res).includes('result')) {
    return res.result
  }
  return res
}

// 头像
export function keybase(identity) {
  return fetch(`https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${identity}&fields=pictures`)
    .then(res => res.json())
}

export default class ChainFetch {
  constructor() {
    this.osmosis = new OsmosAPI()
    this.EndpointVersion = {
      certik: 'v1alpha1',
    }
  }

  getEndpointVersion() {
    return this.EndpointVersion[this.config.chain_name] || 'v1beta1'
  }

  getSelectedConfig() {
    let chain = store.state.chains.selected
    const lschains = localStorage.getItem('chains')
    if (lschains) {
      chain = JSON.parse(lschains)[chain?.chain_name || 'cosmos']
    }
    if (!chain.sdk_version) {
      chain.sdk_version = '0.33'
    }
    this.config = chain
    return this.config
  }

  isModuleLoaded(name) {
    if (this.config.unload_module) {
      return !this.config.unload_module.includes(name)
    }
    return true
  }

  async getNodeInfo() {
    return this.get('/cosmos/base/tendermint/v1beta1/node_info')
  }

  async getLatestBlock(config = null) {
    const conf = config || this.getSelectedConfig()
    const ver = conf.sdk_version || '0.41'
    if (ver && compareVersions(ver, '0.45') < 1) {
      return this.get('/blocks/latest', config).then(data => Block.create(data)).then(block => {
        block.block.last_commit.signatures.map(s1 => {
          const s = s1
          s.validator_address = toBase64(fromHex(s.validator_address))
          return s
        })
        return block
      })
    }
    return this.get('/cosmos/base/tendermint/v1beta1/blocks/latest', config).then(data => Block.create(data))
  }

  async getBlockByHeight(height, config = null) {
    const conf = config || this.getSelectedConfig()
    const ver = conf.sdk_version || '0.41'
    if (ver && compareVersions(ver, '0.45') < 1) {
      return this.get('/blocks/latest', config).then(data => Block.create(data)).then(block => {
        block.block.last_commit.signatures.map(s1 => {
          const s = s1
          s.validator_address = toBase64(fromHex(s.validator_address))
          return s
        })
        return block
      })
    }
    return this.get(`/cosmos/base/tendermint/v1beta1/blocks/${height}`, config).then(data => Block.create(data))
  }

  async getSlashingSigningInfo(config = null) {
    return this.get('/cosmos/slashing/v1beta1/signing_infos?pagination.limit=500', config)
  }

  async getTxs(hash, config = null) {
    const conf = config || this.getSelectedConfig()
    const ver = conf.sdk_version || '0.41'
    // /cosmos/tx/v1beta1/txs/{hash}
    if (ver && compareVersions(ver, '0.40') < 1) {
      return this.get(`/txs/${hash}`).then(data => WrapStdTx.create(data, ver))
    }
    return this.get(`/cosmos/tx/v1beta1/txs/${hash}`).then(data => WrapStdTx.create(data, ver))
  }

  async getTxsBySender(sender) {
    return this.get(`/cosmos/tx/v1beta1/txs?events=message.sender='${sender}'&pagination.reverse=true&order_by=ORDER_BY_DESC`)
  }

  async getTxsByRecipient(recipient) {
    return this.get(`/cosmos/tx/v1beta1/txs?message.recipient=${recipient}`)
  }

  async getTxsByHeight(height) {
    return this.get(`/cosmos/tx/v1beta1/txs?events=tx.height=${height}`)
  }

  async getValidatorDistribution(address) {
    // return this.get(`/distribution/validators/${address}`).then(data => {
    //   const value = commonProcess(data)
    //   const ret = ValidatorDistribution.create({
    //     operator_address: address,
    //     self_bond_rewards: value.self_bond_rewards,
    //     val_commission: value.val_commission.commission,
    //   })
    //   return ret
    // })
    return Promise.all([
      this.get(`/cosmos/distribution/v1beta1/validators/${address}/commission`),
      this.get(`/cosmos/distribution/v1beta1/validators/${address}/outstanding_rewards`),
    ]).then(data => {
      const ret = ValidatorDistribution.create({
        operator_address: address,
        self_bond_rewards: data[1].rewards.rewards,
        val_commission: data[0].commission.commission,
      })
      return ret
    })
  }

  async getStakingDelegatorDelegation(delegatorAddr, validatorAddr) {
    return this.get(`/cosmos/staking/v1beta1/validators/${validatorAddr}/delegations/${delegatorAddr}`).then(data => StakingDelegation.create(commonProcess(data).delegation_response))
  }

  async getBankTotal(denom) {
    if (compareVersions(this.config.sdk_version, '0.40') < 0) {
      return this.get(`/supply/total/${denom}`).then(data => ({ amount: commonProcess(data), denom }))
    }
    return this.get(`/cosmos/bank/v1beta1/supply/${denom}`).then(data => commonProcess(data).amount)
  }

  async getBankTotals() {
    if (compareVersions(this.config.sdk_version, '0.40') < 0) {
      return this.get('/supply/total').then(data => commonProcess(data))
    }
    return this.get('/cosmos/bank/v1beta1/supply').then(data => data.supply)
  }

  async getStakingPool() {
    return this.get('/cosmos/staking/v1beta1/pool').then(data => new StakingPool().init(commonProcess(data.pool)))
  }

  async getMintingInflation() {
    if (this.config.chain_name === 'evmos') {
      return this.get('/evmos/inflation/v1/inflation_rate').then(data => Number(data.inflation_rate / 100 || 0))
    }
    if (this.config.chain_name === 'echelon') {
      return this.get('/echelon/inflation/v1/inflation_rate').then(data => Number(data.inflation_rate / 100 || 0))
    }
    if (this.isModuleLoaded('minting')) {
      return this.get('/cosmos/mint/v1beta1/inflation').then(data => Number(commonProcess(data.inflation)))
    }
    return 0
  }

  async getStakingParameters() {
    return this.get('/cosmos/staking/v1beta1/params').then(data => {
      this.getSelectedConfig()
      return StakingParameters.create(commonProcess(data.params), this.config.chain_name)
    })
  }

  async getValidatorList(config = null) {
    return this.get('/cosmos/staking/v1beta1/validators?pagination.limit=200&status=BOND_STATUS_BONDED', config).then(data => {
      const vals = commonProcess(data.validators).map(i => new Validator().init(i))
      try {
        localStorage.setItem(`validators-${this.config.chain_name}`, JSON.stringify(vals))
      } catch (err) {
        // clear cache
        for (let i = 0; i < localStorage.length; i += 1) {
          const key = localStorage.key(i)
          if (key.startsWith('validators')) {
            localStorage.removeItem(key)
          }
        }
        // set again
        localStorage.setItem(`validators-${this.config.chain_name}`, JSON.stringify(vals))
      }
      return vals
    })
  }

  async getValidatorUnbondedList() {
    return this.get('/cosmos/staking/v1beta1/validators?pagination.limit=100&status=BOND_STATUS_UNBONDED').then(data => {
      const result = commonProcess(data.validators)
      const vals = result.validators ? result.validators : result
      return vals.map(i => new Validator().init(i))
    })
  }

  async getValidatorListByStatus(status) {
    return this.get(`/cosmos/staking/v1beta1/validators?status=${status}&pagination.limit=500`).then(data => {
      const result = commonProcess(data)
      const vals = result.validators ? result.validators : result
      return vals.map(i => new Validator().init(i))
    })
  }

  async getValidatorListByHeight(height, offset) {
    return this.get(`/cosmos/base/tendermint/v1beta1/validatorsets/${height}?pagination.limit=100&pagination.offset=${offset}`).then(data => commonProcess(data))
  }

  async getStakingValidator(address) {
    return this.get(`/cosmos/staking/v1beta1/validators/${address}`).then(data => new Validator().init(commonProcess(data).validator))
  }

  async getSlashingParameters() {
    if (this.isModuleLoaded('slashing')) {
      return this.get('/cosmos/slashing/v1beta1/params').then(data => commonProcess(data.params))
    }
    return null
  }

  async getMintParameters() {
    if (this.config.chain_name === 'evmos') {
      const result = await this.get('/evmos/inflation/v1/params').then(data => data.params)
      await this.get('/evmos/inflation/v1/period').then(data => {
        Object.entries(data).forEach(x => {
          const k = x[0]
          const v = x[1]
          result[k] = v
        })
      })
      await this.get('/evmos/inflation/v1/total_supply').then(data => {
        Object.entries(data).forEach(x => {
          const k = x[0]
          const v = x[1]
          result[k] = v
        })
      })
      return result
    }
    if (this.config.chain_name === 'echelon') {
      const result = await this.get('/echelon/inflation/v1/params').then(data => data.params)
      await this.get('/echelon/inflation/v1/period').then(data => {
        Object.entries(data).forEach(x => {
          const k = x[0]
          const v = x[1]
          result[k] = v
        })
      })
      await this.get('/echelon/inflation/v1/total_supply').then(data => {
        Object.entries(data).forEach(x => {
          const k = x[0]
          const v = x[1]
          result[k] = v
        })
      })
      return result
    }
    if (this.isModuleLoaded('minting')) {
      return this.get('/cosmos/mint/v1beta1/params').then(data => commonProcess(data.params))
    }
    return null
  }

  async getDistributionParameters() {
    return this.get('/cosmos/distribution/v1beta1/params').then(data => commonProcess(data.params))
  }

  async getGovernanceParameterDeposit() {
    return this.get('/cosmos/gov/v1beta1/params/deposit').then(data => commonProcess(data.deposit_params))
  }

  async getGovernanceParameterTallying() {
    return this.get('/cosmos/gov/v1beta1/params/tallying').then(data => commonProcess(data.tally_params))
  }

  async getGovernanceParameterVoting() {
    return this.get('/cosmos/gov/v1beta1/params/voting').then(data => commonProcess(data.voting_params))
  }

  async getGovernanceTally(pid, total, conf) {
    return this.get(`/cosmos/gov/v1beta1/proposals/${pid}/tally`, conf).then(data => new ProposalTally().init(commonProcess(data).tally, total))
  }

  getGovernance(pid) {
    return this.get(`/cosmos/gov/v1beta1/proposals/${pid}`).then(data => {
      const p = new Proposal().init(commonProcess(data).proposal, 0)
      p.versionFixed(this.config.sdk_version)
      return p
    })
  }

  async getGovernanceProposer(pid) {
    if (this.config.chain_name === 'certik') {
      return this.get(`/shentu/gov/v1alpha1/${pid}/proposer`).then(data => new Proposer().init(commonProcess(data)))
    }
    return this.get(`/gov/proposals/${pid}/proposer`).then(data => new Proposer().init(commonProcess(data)))
  }

  async getGovernanceDeposits(pid) {
    if (this.config.chain_name === 'certik') {
      return this.get(`/shentu/gov/v1alpha1/proposals/${pid}/deposits`).then(data => {
        const result = commonProcess(data)
        return Array.isArray(result) ? result.reverse().map(d => new Deposit().init(d)) : result
      })
    }
    return this.get(`/cosmos/gov/v1beta1/proposals/${pid}/deposits`).then(data => {
      const result = commonProcess(data)
      return Array.isArray(result) ? result.reverse().map(d => new Deposit().init(d)) : result
    })
  }

  async getGovernanceVotes(pid, next = '', limit = 50) {
    if (compareVersions(this.config.sdk_version, '0.40') < 0) {
      return this.get(`/cosmos/gov/v1beta1/proposals/${pid}/votes`).then(data => ({
        votes: commonProcess(data).map(d => new Votes().init(d)),
        pagination: {},
      }))
    }
    if (this.config.chain_name === 'shentu') {
      return this.get(`/shentu/gov/v1alpha1/proposals/${pid}/votes?pagination.key=${encodeURIComponent(next)}&pagination.limit=${limit}&pagination.reverse=true`)
    }
    return this.get(`/cosmos/gov/v1beta1/proposals/${pid}/votes?pagination.key=${encodeURIComponent(next)}&pagination.limit=${limit}&pagination.reverse=true`)
  }

  async getGovernanceListByStatus(status, chain = null) {
    const conf = chain || this.config
    const url = conf.chain_name === 'shentu' ? `/shentu/gov/v1alpha1/proposals?pagination.limit=100&proposal_status=${status}` : `/cosmos/gov/v1beta1/proposals?pagination.limit=100&proposal_status=${status}`
    return this.get(url, conf).then(data => {
      let proposals = commonProcess(data)
      if (Array.isArray(proposals.proposals)) {
        proposals = proposals.proposals
      }
      const ret = []
      if (proposals) {
        proposals.forEach(e => {
          const g = new Proposal().init(e, 0)
          g.versionFixed(this.config.sdk_version)
          ret.push(g)
        })
      }
      return {
        proposals: ret,
        pagination: data.pagination,
      }
    })
  }

  async getGovernanceProposalVote(pid, voter, chain) {
    const url = this.config.chain_name === 'shentu'
      ? `/shentu/gov/v1alpha1/proposals/${pid}/votes/${voter}`
      : `/cosmos/gov/v1beta1/proposals/${pid}/votes/${voter}`
    return this.get(url, chain).then(data => {
      if (data.code === 3) {
        throw new Error('not found')
      }
      return data
    })
  }

  /// does NOT return value as expected
  async getUpgradeCurrentPlan(chain = null) {
    return this.get('/cosmos/upgrade/v1beta1/current_plan', chain)
  }

  async getGovernanceList(next = '', chain = null) {
    const key = next || ''
    const url = this.config.chain_name === 'shentu'
      ? `/shentu/gov/v1alpha1/proposals?pagination.limit=20&pagination.reverse=true&pagination.key=${key}`
      : `/cosmos/gov/v1beta1/proposals?pagination.limit=20&pagination.reverse=true&pagination.key=${key}`
    return this.get(url, chain).then(data => {
      let proposals = commonProcess(data)
      if (Array.isArray(proposals.proposals)) {
        proposals = proposals.proposals
      }
      const ret = []
      if (proposals) {
        proposals.forEach(e => {
          const g = new Proposal().init(e, 0)
          g.versionFixed(this.config.sdk_version)
          ret.push(g)
        })
      }
      return {
        proposals: ret,
        pagination: data.pagination,
      }
    })
  }

  async getAuthAccount(address, config = null) {
    return this.get('/cosmos/auth/v1beta1/accounts/'.concat(address), config).then(data => {
      const result = commonProcess(data)
      return result
    })
  }

  async getBankAccountBalance(address) {
    return this.get('/cosmos/bank/v1beta1/balances/'.concat(address)).then(data => commonProcess(data).balances)
  }

  async getStakingReward(address, config = null) {
    if (compareVersions(config ? config.sdk_version : this.config.sdk_version, '0.40') < 0) {
      return this.get(`/distribution/delegators/${address}/rewards`, config).then(data => commonProcess(data))
    }
    return this.get(`/cosmos/distribution/v1beta1/delegators/${address}/rewards`, config).then(data => commonProcess(data))
  }

  async getValidatorSlashs(address, config = null) {
    return this.get(`/cosmos/distribution/v1beta1/validators//${address}/slashes`, config).then(data => commonProcess(data))
  }

  async getStakingValidators(address) {
    return this.get(`/cosmos/distribution/v1beta1/delegators/${address}/validators?pagination.size=200`).then(data => commonProcess(data.validators))
  }

  async getStakingDelegations(address, config = null) {
    if (compareVersions(config ? config.sdk_version : this.config.sdk_version, '0.40') < 0) {
      return this.get(`/staking/delegators/${address}/delegations`, config).then(data => commonProcess(data).map(x => {
        const xh = x
        if (!xh.delegation) {
          xh.delegation = {
            validator_address: x.validator_address,
            delegator_address: x.delegator_address,
          }
        }
        return xh
      }))
    }
    return this.get(`/cosmos/staking/v1beta1/delegations/${address}`, config).then(data => commonProcess(data))
  }

  async getStakingRedelegations(address, config = null) {
    if (compareVersions(config ? config.sdk_version : this.config.sdk_version, '0.40') < 0) {
      return this.get(`/staking/redelegations?delegator=${address}`, config).then(data => commonProcess(data))
    }
    return this.get(`/cosmos/staking/v1beta1/delegators/${address}/redelegations`, config).then(data => commonProcess(data))
  }

  async getStakingUnbonding(address, config = null) {
    if (compareVersions(config ? config.sdk_version : this.config.sdk_version, '0.40') < 0) {
      return this.get(`/staking/delegators/${address}/unbonding_delegations`, config).then(data => commonProcess(data))
    }
    return this.get(`/cosmos/staking/v1beta1/delegators/${address}/unbonding_delegations`, config).then(data => commonProcess(data))
  }

  async getBankBalances(address, config = null) {
    return this.get('/cosmos/bank/v1beta1/balances/'.concat(address), config).then(data => commonProcess(data))
  }

  async getCommunityPool(config = null) {
    return this.get('/cosmos/distribution/v1beta1/community_pool', config).then(data => commonProcess(data))
  }

  async getAllIBCDenoms(config = null) {
    const conf = config || this.getSelectedConfig()
    const sdkVersion = conf.sdk_version
    if (compareVersions(sdkVersion, '0.44.2') < 0) {
      return this.get('/ibc/applications/transfer/v1beta1/denom_traces?pagination.limit=500', conf).then(data => commonProcess(data))
    }
    return this.get('/ibc/apps/transfer/v1/denom_traces?pagination.limit=500', conf).then(data => commonProcess(data))
  }

  async getIBCDenomTrace(hash, config = null) {
    const h = hash.substring(hash.indexOf('/') + 1)
    const sdkVersion = config ? config.sdk_version : this.config.sdk_version
    if (compareVersions(sdkVersion, '0.44.2') < 0) {
      return this.get('/ibc/applications/transfer/v1beta1/denom_traces/'.concat(h), config).then(data => commonProcess(data))
    }
    return this.get('/ibc/apps/transfer/v1/denom_traces/'.concat(h), config).then(data => commonProcess(data))
  }

  async getIBCChannels(config = null, key = null) {
    if (key) {
      return this.get('/ibc/core/channel/v1/channels?pagination.key='.concat(key), config).then(data => commonProcess(data))
    }
    return this.get('/ibc/core/channel/v1/channels?pagination.limit=1000', config).then(data => commonProcess(data))
  }

  // eslint-disable-next-line camelcase
  async getIBCChannelClientState(channel_id, port_id, config = null) {
    // eslint-disable-next-line camelcase
    return this.get(`/ibc/core/channel/v1/channels/${channel_id}/ports/${port_id}/client_state`, config).then(data => commonProcess(data))
  }

  // eslint-disable-next-line camelcase
  async getIBCChannel(channel_id, port_id, config = null) {
    // eslint-disable-next-line camelcase
    return this.get(`/ibc/core/channel/v1/channels/${channel_id}/ports/${port_id}`, config).then(data => commonProcess(data))
  }

  static async getBankBalance(baseurl, address) {
    return ChainFetch.fetch(baseurl, '/cosmos/bank/v1beta1/balances/'.concat(address)).then(data => commonProcess(data))
  }

  async getGravityPools() {
    return this.get('/cosmos/liquidity/v1beta1/pools').then(data => commonProcess(data))
  }

  async getMarketChart(days = 14, coin = null) {
    const conf = this.getSelectedConfig()
    const currency = getUserCurrency()
    if (conf.assets[0] && conf.assets[0].coingecko_id) {
      return ChainFetch.fetch(' https://api.coingecko.com', `/api/v3/coins/${coin || conf.assets[0].coingecko_id}/market_chart?vs_currency=${currency}&days=${days}`)
    }
    return null
  }

  async getCoinInfo(coin = null) {
    const conf = this.getSelectedConfig()
    if (conf.assets[0] && conf.assets[0].coingecko_id) {
      return ChainFetch.fetch(' https://api.coingecko.com', `/api/v3/coins/${coin || conf.assets[0].coingecko_id}`)
    }
    return null
  }

  // CoinMarketCap
  static async fetchCoinMarketCap(url) {
    const host = 'https://price.ping.pub'
    return fetch(host + url).then(response => response.json())
  }

  static async fetchTokenQuote(symbol) {
    return ChainFetch.fetchCoinMarketCap(`/quote/${symbol}`)
  }

  // Simulate Execution of tx
  async simulate(bodyBytes, config = null) {
    const txString = toBase64(TxRaw.encode(bodyBytes).finish())
    const txRaw = {
      tx_bytes: txString,
    }
    return this.post('/cosmos/tx/v1beta1/simulate', txRaw, config)
  }

  // Tx Submit
  async broadcastTx(bodyBytes, config = null) {
    const txbytes = bodyBytes.authInfoBytes ? TxRaw.encode(bodyBytes).finish() : bodyBytes
    const txString = toBase64(txbytes)
    const txRaw = {
      tx_bytes: txString,
      mode: 'BROADCAST_MODE_SYNC', // BROADCAST_MODE_SYNC, BROADCAST_MODE_BLOCK, BROADCAST_MODE_ASYNC
    }
    return this.post('/cosmos/tx/v1beta1/txs', txRaw, config).then(res => {
      if (res.code && res.code !== 0) {
        throw new Error(res.message)
      }
      if (res.tx_response && res.tx_response.code !== 0) {
        throw new Error(res.tx_response.raw_log)
      }
      return res
    })
  }

  async post(url = '', data = {}, config = null) {
    if (!config) {
      this.getSelectedConfig()
    }
    const conf = config || this.config
    const index = this.getApiIndex(config)
    // Default options are marked with *
    const response = await fetch((Array.isArray(conf.api) ? conf.api[index] : conf.api) + url, {
      method: 'POST', // *GET, POST, PUT, DELETE, etc.
      // mode: 'cors', // no-cors, *cors, same-origin
      // credentials: 'same-origin', // redirect: 'follow', // manual, *follow, error
      // referrerPolicy: 'origin', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
      headers: {
        'Content-Type': 'text/plain',
        Accept: '*/*',
        'Accept-Encoding': 'gzip, deflate, br',
      },
      body: JSON.stringify(data), // body data type must match "Content-Type" header
    })
    // const response = axios.post((config ? config.api : this.config.api) + url, data)
    return response.json() // parses JSON response into native JavaScript objects
  }

  async get(url, config = null) {
    if (!config) {
      this.getSelectedConfig()
    }
    const conf = config || this.config
    const finalurl = (Array.isArray(conf.api) ? conf.api[this.getApiIndex(config)] : conf.api) + url
    // finalurl = finalurl.replaceAll('v1beta1', this.getEndpointVersion())
    const ret = await fetch(finalurl).then(response => response.json())
    return ret
  }

  getApiIndex(config = null) {
    const conf = config || this.config
    const index = Number(localStorage.getItem(`${conf.chain_name}-api-index`) || 0)
    return index < conf.api.length ? index : 0
  }

  async getUrl(url) {
    this.getSelectedConfig()
    return fetch(url).then(res => res.json())
  }

  static fetch(host, url) {
    const ret = fetch((Array.isArray(host) ? host[0] : host) + url).then(response => response.json())
    return ret
  }
}

// export default chainAPI
