import { MsgExecuteContract } from '@delphi-labs/shuttle-react'
import BigNumber from 'bignumber.js'
import moment from 'moment'
import { isMobile } from 'react-device-detect'
import { GetState, SetState } from 'zustand'

import { ENV } from 'constants/env'
import { BN_ZERO } from 'constants/math'
import { Store } from 'store'
import { BNCoin } from 'types/classes/BNCoin'
import { ExecuteMsg as AccountNftExecuteMsg } from 'types/generated/mars-account-nft/MarsAccountNft.types'
import {
  Action,
  ActionCoin,
  Action as CreditManagerAction,
  ExecuteMsg as CreditManagerExecuteMsg,
} from 'types/generated/mars-credit-manager/MarsCreditManager.types'
import { AccountKind } from 'types/generated/mars-rover-health-types/MarsRoverHealthTypes.types'
import { getAssetByDenom, getAssetBySymbol, getPythAssets } from 'utils/assets'
import { generateErrorMessage, getSingleValueFromBroadcastResult } from 'utils/broadcast'
import checkAutoLendEnabled from 'utils/checkAutoLendEnabled'
import { defaultFee } from 'utils/constants'
import { formatAmountWithSymbol } from 'utils/formatters'
import getTokenOutFromSwapResponse from 'utils/getTokenOutFromSwapResponse'
import { BN } from 'utils/helpers'
import { getVaultDepositCoinsFromActions } from 'utils/vaults'

function generateExecutionMessage(
  sender: string | undefined = '',
  contract: string,
  msg: CreditManagerExecuteMsg | AccountNftExecuteMsg | PythUpdateExecuteMsg,
  funds: Coin[],
) {
  return new MsgExecuteContract({
    sender,
    contract,
    msg,
    funds,
  })
}

export default function createBroadcastSlice(
  set: SetState<Store>,
  get: GetState<Store>,
): BroadcastSlice {
  const handleResponseMessages = (props: HandleResponseProps) => {
    const { id, accountId, response, action, lend, changes, target, message } = props
    if (!response) return

    if (response.error || response.result?.response.code !== 0) {
      set({
        toast: {
          id,
          message: generateErrorMessage(response),
          isError: true,
          hash: response.result?.hash,
        },
      })
      return
    }

    const toast: ToastResponse = {
      id,
      accountId: accountId,
      isError: false,
      hash: response?.result?.hash,
      content: [],
      timestamp: moment().unix(),
      address: get().address ?? '',
    }

    if (message) {
      toast.message = message
      set({ toast })
      return
    }

    if (!changes) return

    switch (action) {
      case 'borrow':
        const borrowCoin = changes.debts ? [changes.debts[0].toCoin()] : []
        const borrowAction = lend ? 'Borrowed and lent' : 'Borrowed'
        toast.content.push({
          coins: borrowCoin,
          text: target === 'wallet' ? 'Borrowed to wallet' : borrowAction,
        })
        break

      case 'withdraw':
        toast.content.push({
          coins: changes.deposits?.map((deposit) => deposit.toCoin()) ?? [],
          text: target === 'wallet' ? 'Withdrew to Wallet' : 'Unlent',
        })
        break

      case 'deposit':
        toast.content.push({
          coins: changes.deposits?.map((deposit) => deposit.toCoin()) ?? [],
          text: lend ? 'Deposited and lent' : 'Deposited',
        })
        break

      case 'lend':
        const lendCoin = changes.lends ? [changes.lends[0].toCoin()] : []
        toast.content.push({
          coins: lendCoin,
          text: 'Lent',
        })
        break

      case 'repay':
        const repayCoin = changes.deposits ? [changes.deposits[0].toCoin()] : []
        toast.content.push({
          coins: repayCoin,
          text: 'Repaid',
        })
        break

      case 'vault':
      case 'vaultCreate':
        toast.content.push({
          coins: changes.deposits?.map((debt) => debt.toCoin()) ?? [],
          text: action === 'vaultCreate' ? 'Created a Vault Position' : 'Added to Vault Position',
        })
        break
    }

    set({ toast })
    return
  }

  const getEstimatedFee = async (messages: MsgExecuteContract[]) => {
    if (!get().client) {
      return defaultFee
    }
    try {
      const simulateResult = await get().client?.simulate({
        messages,
        wallet: get().client?.connectedWallet,
      })

      if (simulateResult) {
        const { success, fee } = simulateResult

        if (success) {
          return {
            amount: fee ? fee.amount : [],
            gas: BN(fee ? fee.gas : 0).toFixed(0),
          }
        }
      }

      throw 'Simulation failed'
    } catch (ex) {
      return defaultFee
    }
  }

  return {
    toast: null,
    addToStakingStrategy: async (options: {
      accountId: string
      actions: Action[]
      depositCoin: BNCoin
      borrowCoin: BNCoin
    }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: options.actions,
        },
      }

      const response = get().executeMsg({
        messages: [
          generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [
            options.depositCoin.toCoin(),
          ]),
        ],
      })

      const swapOptions = { denomOut: options.depositCoin.denom, coinIn: options.borrowCoin }

      get().setToast({
        response,
        options: {
          action: 'hls-staking',
          accountId: options.accountId,
          changes: { deposits: [options.depositCoin], debts: [options.borrowCoin] },
        },
        swapOptions,
      })

      return response.then((response) => !!response.result)
    },
    borrow: async (options: { accountId: string; coin: BNCoin; borrowToWallet: boolean }) => {
      const borrowAction: Action = { borrow: options.coin.toCoin() }
      const withdrawAction: Action = { withdraw: options.coin.toActionCoin() }
      const actions = options.borrowToWallet ? [borrowAction, withdrawAction] : [borrowAction]

      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions,
        },
      }

      if (
        !options.borrowToWallet &&
        checkAutoLendEnabled(options.accountId) &&
        getAssetByDenom(options.coin.denom)?.isAutoLendEnabled
      ) {
        msg.update_credit_account.actions.push({
          lend: { denom: options.coin.denom, amount: 'account_balance' },
        })
      }
      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'borrow',
          lend: checkAutoLendEnabled(options.accountId),
          target: options.borrowToWallet ? 'wallet' : 'account',
          accountId: options.accountId,
          changes: { debts: [options.coin] },
        },
      })

      return response.then((response) => !!response.result)
    },
    changeHlsStakingLeverage: async (options: { accountId: string; actions: Action[] }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: options.actions,
        },
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'deposit',
          message: `Changed Leverage`,
        },
      })

      return response.then((response) => !!response.result)
    },
    closeHlsStakingPosition: async (options: { accountId: string; actions: Action[] }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: options.actions,
        },
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'deposit',
          message: `Exited HLS strategy`,
        },
      })

      return response.then((response) => !!response.result)
    },

    createAccount: async (accountKind: AccountKind) => {
      const msg: CreditManagerExecuteMsg = {
        create_credit_account: accountKind,
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'create',
          message: `Created the Credit Account`,
        },
      })

      return response.then((response) =>
        response.result
          ? getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id')
          : null,
      )
    },
    deleteAccount: async (options: { accountId: string; lends: BNCoin[] }) => {
      const reclaimMsg = options.lends.map((coin) => {
        return {
          reclaim: coin.toActionCoin(true),
        }
      })

      const refundMessage: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [...reclaimMsg, { refund_all_coin_balances: {} }],
        },
      }

      const burnMessage: AccountNftExecuteMsg = {
        burn: {
          token_id: options.accountId,
        },
      }

      const response = get().executeMsg({
        messages: [
          generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, refundMessage, []),
          generateExecutionMessage(get().address, ENV.ADDRESS_ACCOUNT_NFT, burnMessage, []),
        ],
      })

      get().setToast({
        response,
        options: {
          action: 'delete',
          accountId: options.accountId,
          message: `Deleted the Credit Account`,
        },
      })

      return response.then((response) => !!response.result)
    },
    claimRewards: (options: { accountId: string }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [
            {
              claim_rewards: {},
            },
          ],
        },
      }

      const messages = [
        generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, []),
      ]
      const estimateFee = () => getEstimatedFee(messages)

      const execute = async () => {
        const response = get().executeMsg({
          messages,
        })

        get().setToast({
          response,
          options: {
            action: 'claim',
            accountId: options.accountId,
            message: `Claimed rewards`,
          },
        })

        return response.then((response) => !!response.result)
      }

      return { estimateFee, execute }
    },
    deposit: async (options: { accountId: string; coins: BNCoin[]; lend: boolean }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: options.coins.map((coin) => ({
            deposit: coin.toCoin(),
          })),
        },
      }

      if (options.lend) {
        msg.update_credit_account.actions.push(
          ...options.coins
            .filter((coin) => getAssetByDenom(coin.denom)?.isAutoLendEnabled)
            .map((coin) => ({ lend: coin.toActionCoin(options.lend) })),
        )
      }

      const funds = options.coins.map((coin) => coin.toCoin())

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, funds)],
      })

      get().setToast({
        response,
        options: {
          action: 'deposit',
          lend: options.lend,
          accountId: options.accountId,
          changes: { deposits: options.coins },
        },
      })

      return response.then((response) => !!response.result)
    },
    unlock: async (options: { accountId: string; vault: DepositedVault; amount: string }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [
            {
              request_vault_unlock: {
                vault: { address: options.vault.address },
                amount: options.amount,
              },
            },
          ],
        },
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'unlock',
          accountId: options.accountId,
          message: `Requested unlock for ${options.vault.name}`,
        },
      })

      return response.then((response) => !!response.result)
    },

    withdrawFromVaults: async (options: {
      accountId: string
      vaults: DepositedVault[]
      slippage: number
    }) => {
      const actions: CreditManagerAction[] = []
      options.vaults.forEach((vault) => {
        if (vault.unlockId) {
          actions.push({
            exit_vault_unlocked: {
              id: vault.unlockId,
              vault: { address: vault.address },
            },
          })
          actions.push({
            withdraw_liquidity: {
              lp_token: { denom: vault.denoms.lp, amount: 'account_balance' },
              slippage: options.slippage.toString(),
            },
          })
        }
      })
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions,
        },
      }

      if (checkAutoLendEnabled(options.accountId)) {
        for (const vault of options.vaults) {
          for (const symbol of Object.values(vault.symbols)) {
            const asset = getAssetBySymbol(symbol)
            if (asset?.isAutoLendEnabled) {
              msg.update_credit_account.actions.push({
                lend: { denom: asset.denom, amount: 'account_balance' },
              })
            }
          }
        }
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      const vaultsString = options.vaults.length === 1 ? 'vault' : 'vaults'

      get().setToast({
        response,
        options: {
          action: 'withdraw',
          accountId: options.accountId,
          message: `Withdrew ${options.vaults.length} unlocked ${vaultsString} to the account`,
        },
      })

      return response.then((response) => !!response.result)
    },
    depositIntoVault: async (options: {
      accountId: string
      actions: Action[]
      deposits: BNCoin[]
      borrowings: BNCoin[]
      isCreate: boolean
      kind: AccountKind
    }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: options.actions,
        },
      }

      const response = get().executeMsg({
        messages: [
          generateExecutionMessage(
            get().address,
            ENV.ADDRESS_CREDIT_MANAGER,
            msg,
            options.kind === 'default' ? [] : options.deposits.map((coin) => coin.toCoin()),
          ),
        ],
      })

      const depositedCoins = getVaultDepositCoinsFromActions(options.actions)

      get().setToast({
        response,
        options: {
          action: options.isCreate ? 'vaultCreate' : 'vault',
          accountId: options.accountId,
          changes: { deposits: depositedCoins },
        },
      })

      return response.then((response) => !!response.result)
    },
    withdraw: async (options: {
      accountId: string
      coins: Array<{ coin: BNCoin; isMax?: boolean }>
      borrow: BNCoin[]
      reclaims: ActionCoin[]
    }) => {
      const reclaimActions = options.reclaims.map((coin) => ({
        reclaim: coin,
      }))
      const withdrawActions = options.coins.map(({ coin, isMax }) => ({
        withdraw: coin.toActionCoin(isMax),
      }))
      const borrowActions = options.borrow.map((coin) => ({
        borrow: coin.toCoin(),
      }))

      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [...reclaimActions, ...borrowActions, ...withdrawActions],
        },
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'withdraw',
          target: 'wallet',
          accountId: options.accountId,
          changes: { deposits: options.coins.map((coin) => coin.coin) },
        },
      })

      return response.then((response) => !!response.result)
    },
    repay: async (options: {
      accountId: string
      coin: BNCoin
      accountBalance?: boolean
      lend?: BNCoin
      fromWallet?: boolean
    }) => {
      const actions: Action[] = [
        {
          repay: {
            coin: options.coin.toActionCoin(options.accountBalance),
          },
        },
      ]

      if (options.lend && options.lend.amount.isGreaterThan(0))
        actions.unshift({ reclaim: options.lend.toActionCoin() })

      const msg: CreditManagerExecuteMsg = options.fromWallet
        ? {
            repay_from_wallet: {
              account_id: options.accountId,
            },
          }
        : {
            update_credit_account: {
              account_id: options.accountId,
              actions,
            },
          }

      const response = get().executeMsg({
        messages: [
          generateExecutionMessage(
            get().address,
            ENV.ADDRESS_CREDIT_MANAGER,
            msg,
            options.fromWallet ? [options.coin.toCoin()] : [],
          ),
        ],
      })

      get().setToast({
        response,
        options: {
          action: 'repay',
          accountId: options.accountId,
          changes: { deposits: [options.coin] },
        },
      })

      return response.then((response) => !!response.result)
    },
    lend: async (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [
            {
              lend: options.coin.toActionCoin(options.isMax),
            },
          ],
        },
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'lend',
          accountId: options.accountId,
          changes: { lends: [options.coin] },
        },
      })

      return response.then((response) => !!response.result)
    },
    reclaim: async (options: { accountId: string; coin: BNCoin; isMax?: boolean }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [
            {
              reclaim: options.coin.toActionCoin(options.isMax),
            },
          ],
        },
      }

      const response = get().executeMsg({
        messages: [generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, [])],
      })

      get().setToast({
        response,
        options: {
          action: 'withdraw',
          target: 'account',
          accountId: options.accountId,
          changes: { deposits: [options.coin] },
        },
      })

      return response.then((response) => !!response.result)
    },
    swap: (options: {
      accountId: string
      coinIn: BNCoin
      reclaim?: BNCoin
      borrow?: BNCoin
      denomOut: string
      slippage: number
      isMax?: boolean
      repay: boolean
    }) => {
      const msg: CreditManagerExecuteMsg = {
        update_credit_account: {
          account_id: options.accountId,
          actions: [
            ...(options.reclaim ? [{ reclaim: options.reclaim.toActionCoin() }] : []),
            ...(options.borrow ? [{ borrow: options.borrow.toCoin() }] : []),
            {
              swap_exact_in: {
                coin_in: options.coinIn.toActionCoin(options.isMax),
                denom_out: options.denomOut,
                slippage: options.slippage.toString(),
              },
            },
            ...(options.repay
              ? [
                  {
                    repay: {
                      coin: BNCoin.fromDenomAndBigNumber(options.denomOut, BN_ZERO).toActionCoin(
                        true,
                      ),
                    },
                  },
                ]
              : []),
          ],
        },
      }

      if (
        checkAutoLendEnabled(options.accountId) &&
        getAssetByDenom(options.denomOut)?.isAutoLendEnabled &&
        !options.repay
      ) {
        msg.update_credit_account.actions.push({
          lend: { denom: options.denomOut, amount: 'account_balance' },
        })
      }

      const messages = [
        generateExecutionMessage(get().address, ENV.ADDRESS_CREDIT_MANAGER, msg, []),
      ]

      const estimateFee = () => getEstimatedFee(messages)

      const execute = async () => {
        const response = get().executeMsg({
          messages,
        })

        const swapOptions = { denomOut: options.denomOut, coinIn: options.coinIn }

        get().setToast({
          response,
          options: {
            action: 'swap',
            accountId: options.accountId,
          },
          swapOptions,
        })

        return response.then((response) => !!response.result)
      }

      return { estimateFee, execute }
    },
    updateOracle: async (pricesData: string[]) => {
      const msg: PythUpdateExecuteMsg = { update_price_feeds: { data: pricesData } }
      const pythAssets = getPythAssets()
      const response = get().executeMsg({
        messages: [
          generateExecutionMessage(get().address, ENV.ADDRESS_PYTH, msg, [
            { denom: get().baseCurrency.denom, amount: String(pythAssets.length) },
          ]),
        ],
      })

      get().setToast({
        response,
        options: {
          action: 'oracle',
          message: 'Oracle updated successfully!',
        },
      })

      return response.then((response) => !!response.result)
    },
    setToast: (toast: ToastObject) => {
      const id = moment().unix()
      set({
        toast: {
          id,
          promise: toast.response,
        },
      })

      toast.response.then((response) => {
        if (toast.options.action === 'create') {
          toast.options.accountId =
            getSingleValueFromBroadcastResult(response.result, 'wasm', 'token_id') ?? undefined
        }

        if (toast.swapOptions) {
          const coinOut = getTokenOutFromSwapResponse(response, toast.swapOptions.denomOut)

          if (toast.options.action === 'swap') {
            toast.options.message = `Swapped ${formatAmountWithSymbol(
              toast.swapOptions.coinIn.toCoin(),
            )} for ${formatAmountWithSymbol(coinOut)}`
          }

          if (toast.options.action === 'hls-staking') {
            const depositAmount: BigNumber = toast.options.changes?.deposits?.length
              ? toast.options.changes.deposits[0].amount
              : BN_ZERO

            coinOut.amount = depositAmount.plus(coinOut.amount).toFixed(0)
            toast.options.message = `Added ${formatAmountWithSymbol(coinOut)}`
          }
        }

        handleResponseMessages({
          id,
          response,
          ...toast.options,
        })
      })
    },
    executeMsg: async (options: { messages: MsgExecuteContract[] }): Promise<BroadcastResult> => {
      try {
        const client = get().client
        if (!client) return { error: 'no client detected' }
        const fee = await getEstimatedFee(options.messages)
        const broadcastOptions = {
          messages: options.messages,
          feeAmount: fee.amount[0].amount,
          gasLimit: fee.gas,
          memo: undefined,
          wallet: client.connectedWallet,
          mobile: isMobile,
        }
        const result = await client.broadcast(broadcastOptions)
        if (result.hash) {
          return { result }
        }

        return {
          result: undefined,
          error: 'Transaction failed',
        }
      } catch (error) {
        const e = error as { message: string }
        console.log(e)
        return { result: undefined, error: e.message }
      }
    },
  }
}
