import { cosmosclient, proto } from "@cosmos-client/core";
import { cosmos } from "@cosmos-client/core/cjs/proto";
import {
  FeeType,
  Fees,
  Network,
  RootDerivationPaths,
  Tx,
  TxFrom,
  TxTo,
  TxType,
} from "../client";
import {
  Asset,
  AssetAtom,
  BaseAmount,
  CosmosChain,
  baseAmount,
  eqAsset,
} from "@dojima-wallet/utils";
import axios from "axios";
import BigNumber from "bignumber.js";
import Long from "long";

import { COSMOS_DECIMAL, DEFAULT_GAS_LIMIT } from "./const";
import { APIQueryParam, TxResponse, UnsignedTxParams } from "./cosmos";
import { ChainId, ChainIds, ClientUrls as ClientUrls } from "./types";

/**
 * Type guard for MsgSend
 *
 * @param {Msg} msg
 * @returns {boolean} `true` or `false`.
 */
export const isMsgSend = (
  msg: unknown
): msg is proto.cosmos.bank.v1beta1.MsgSend =>
  (msg as proto.cosmos.bank.v1beta1.MsgSend)?.amount !== undefined &&
  (msg as proto.cosmos.bank.v1beta1.MsgSend)?.from_address !== undefined &&
  (msg as proto.cosmos.bank.v1beta1.MsgSend)?.to_address !== undefined;

/**
 * Type guard for MsgMultiSend
 *
 * @param {Msg} msg
 * @returns {boolean} `true` or `false`.
 */
export const isMsgMultiSend = (
  msg: unknown
): msg is proto.cosmos.bank.v1beta1.MsgMultiSend =>
  (msg as proto.cosmos.bank.v1beta1.MsgMultiSend)?.inputs !== undefined &&
  (msg as proto.cosmos.bank.v1beta1.MsgMultiSend)?.outputs !== undefined;

/**
 * Get denomination from Asset - currently `ATOM` supported only
 *
 * @param {Asset} asset
 * @returns {string} The denomination of the given asset.
 */
export const getDenom = (asset: Asset): string | null => {
  if (eqAsset(asset, AssetAtom)) return "uatom";
  return null;
};

/**
 * Get Asset from denomination
 *
 * @param {string} denom
 * @returns {Asset|null} The asset of the given denomination.
 */
export const getAsset = (denom: string): Asset | null => {
  if (denom === getDenom(AssetAtom)) return AssetAtom;
  // IBC assets
  if (denom.startsWith("ibc/"))
    // Note: Don't use `assetFromString` here, it will interpret `/` as synth
    return {
      chain: CosmosChain.ticker,
      symbol: denom,
      // At the meantime ticker will be empty
      ticker: "",
      synth: false,
    };
  return null;
};

/**
 * Parses amount from `ICoin[]`
 *
 * @param {ICoin[]} coinst List of coins
 *
 * @returns {BaseAmount} Coin amount
 */
const getCoinAmount = (coins: proto.cosmos.base.v1beta1.ICoin[]): BaseAmount =>
  coins
    .map((coin) => baseAmount(coin.amount || 0, COSMOS_DECIMAL))
    .reduce(
      (acc, cur) => baseAmount(acc.amount().plus(cur.amount()), COSMOS_DECIMAL),
      baseAmount(0, COSMOS_DECIMAL)
    );

/**
 * Filters `ICoin[]` by given `Asset`
 *
 * @param {ICoin[]} coinst List of coins
 * @param {Asset} asset Asset to filter coins
 *
 * @returns {ICoin[]} Filtered list
 */
const getCoinsByAsset = (
  coins: proto.cosmos.base.v1beta1.ICoin[],
  asset: Asset
): proto.cosmos.base.v1beta1.ICoin[] =>
  coins.filter(({ denom }) => {
    const coinAsset = !!denom ? getAsset(denom) : null;
    return !!coinAsset ? eqAsset(coinAsset, asset) : false;
  });

/**
 * Parses transaction history
 *
 * @param {TxResponse[]} txs The transaction response from the node.
 * @param {Asset} asset Asset to get history of transactions from
 *
 * @returns {Tx[]} List of transactions
 */
export const getTxsFromHistory = (txs: TxResponse[], asset: Asset): Tx[] => {
  return (
    txs
      // order list to have latest txs first in list
      .sort((a, b) => {
        if (a.timestamp === b.timestamp) return 0;
        return a.timestamp > b.timestamp ? -1 : 1;
      })
      .reduce((acc, tx) => {
        const msgs = tx.tx?.body.messages ?? [];

        const from: TxFrom[] = [];
        const to: TxTo[] = [];
        msgs.map((msg) => {
          if (isMsgSend(msg)) {
            const msgSend = msg;
            const coins = getCoinsByAsset(msgSend.amount, asset);
            const amount = getCoinAmount(coins);

            let from_index = -1;

            from.forEach((value, index) => {
              if (value.from === msgSend.from_address) from_index = index;
            });

            if (from_index === -1) {
              from.push({
                from: msgSend.from_address,
                amount,
              });
            } else {
              from[from_index].amount = baseAmount(
                from[from_index].amount.amount().plus(amount.amount()),
                COSMOS_DECIMAL
              );
            }

            let to_index = -1;

            to.forEach((value, index) => {
              if (value.to === msgSend.to_address) to_index = index;
            });

            if (to_index === -1) {
              to.push({
                to: msgSend.to_address,
                amount,
              });
            } else {
              to[to_index].amount = baseAmount(
                to[to_index].amount.amount().plus(amount.amount()),
                COSMOS_DECIMAL
              );
            }
          }
        });

        return [
          ...acc,
          {
            asset,
            from,
            to,
            date: new Date(tx.timestamp),
            type:
              from.length > 0 || to.length > 0
                ? TxType.Transfer
                : TxType.Unknown,
            hash: tx.txhash || "",
          },
        ];
      }, [] as Tx[])
  );
};

/**
 * Get Query String
 *
 * @param {APIQueryParam}
 * @returns {string} The query string.
 */
export const getQueryString = (params: APIQueryParam): string => {
  return Object.keys(params)
    .filter((key) => key.length > 0)
    .map((key) =>
      params[key] == null
        ? key
        : `${key}=${encodeURIComponent(params[key].toString())}`
    )
    .join("&");
};

/**
 * Get the default fee.
 *
 * @returns {Fees} The default fee.
 */
export const getDefaultFees = (): Fees => {
  return {
    type: FeeType.FlatFee,
    fast: baseAmount(750, COSMOS_DECIMAL),
    fastest: baseAmount(2500, COSMOS_DECIMAL),
    average: baseAmount(0, COSMOS_DECIMAL),
  };
};

/**
 * Get address prefix based on the network.
 *
 * @returns {string} The address prefix based on the network.
 *
 **/
export const getPrefix = () => "cosmos";

/**
 * Default client urls
 *
 * @returns {ClientUrls} The client urls for Cosmos.
 */
export const getDefaultClientUrls = (): ClientUrls => {
  // const mainClientUrl = "https://api.cosmos.network";
  const mainClientUrl = "https://rest.cosmos.directory/cosmoshub";
  // Note: In case anyone facing into CORS issue, try the following URLs
  // https://lcd-cosmos.cosmostation.io/
  // https://lcd-cosmoshub.keplr.app/
  return {
    [Network.Testnet]: "https://rest.sentry-02.theta-testnet.polypore.xyz",
    [Network.Stagenet]: mainClientUrl,
    [Network.Mainnet]: mainClientUrl,
  };
};

/**
 * Default chain ids
 *
 * @returns {ChainIds} Chain ids for Cosmos.
 */
export const getDefaultChainIds = (): ChainIds => {
  const mainChainId = "cosmoshub-4";
  return {
    [Network.Testnet]: "theta-testnet-001",
    [Network.Stagenet]: mainChainId,
    [Network.Mainnet]: mainChainId,
  };
};

export const getDefaultRootDerivationPaths = (): RootDerivationPaths => ({
  [Network.Mainnet]: `44'/118'/0'/0/`,
  [Network.Testnet]: `44'/118'/0'/0/`,
  [Network.Stagenet]: `44'/118'/0'/0/`,
});

export const protoFee = ({
  denom,
  amount,
  gasLimit = new BigNumber(DEFAULT_GAS_LIMIT),
}: {
  denom: string;
  amount: BaseAmount;
  gasLimit?: BigNumber;
}): proto.cosmos.tx.v1beta1.Fee =>
  new proto.cosmos.tx.v1beta1.Fee({
    amount: [
      {
        denom,
        amount: amount.amount().toFixed(0),
      },
    ],
    gas_limit: Long.fromString(gasLimit.toFixed(0)),
  });

export const protoMsgSend = ({
  from,
  to,
  amount,
  denom,
}: {
  from: string;
  to: string;
  amount: BaseAmount;
  denom: string;
}): proto.cosmos.bank.v1beta1.MsgSend =>
  new proto.cosmos.bank.v1beta1.MsgSend({
    from_address: from,
    to_address: to,
    amount: [
      {
        amount: amount.amount().toFixed(0),
        denom,
      },
    ],
  });

export const protoTxBody = ({
  from,
  to,
  amount,
  denom,
  memo,
}: UnsignedTxParams): proto.cosmos.tx.v1beta1.TxBody => {
  const msg = protoMsgSend({ from, to, amount, denom });

  return new proto.cosmos.tx.v1beta1.TxBody({
    messages: [cosmosclient.codec.instanceToProtoAny(msg)],
    memo,
  });
};

export const protoAuthInfo = ({
  pubKey,
  sequence,
  mode,
  fee,
}: {
  pubKey: cosmosclient.PubKey;
  sequence: Long.Long;
  mode: proto.cosmos.tx.signing.v1beta1.SignMode;
  fee?: cosmos.tx.v1beta1.IFee;
}): proto.cosmos.tx.v1beta1.AuthInfo =>
  new proto.cosmos.tx.v1beta1.AuthInfo({
    signer_infos: [
      {
        public_key: cosmosclient.codec.instanceToProtoAny(pubKey),
        mode_info: {
          single: {
            mode,
          },
        },
        sequence,
      },
    ],
    fee,
  });

/**
 * Helper to get Cosmos' chain id
 * @param {string} url API url
 */
export const getChainId = async (url: string): Promise<ChainId> => {
  const { data } = await axios.get<{ node_info: { network: string } }>(
    `${url}/node_info`
  );
  return data?.node_info?.network || Promise.reject("Could not parse chain id");
};

/**
 * Helper to get Cosmos' chain id for all networks
 * @param {ClientUrl} urls urls (use `getDefaultClientUrl()` if you don't need to use custom urls)
 */
export const getChainIds = async (urls: ClientUrls): Promise<ChainIds> => {
  return Promise.all([
    getChainId(urls[Network.Testnet]),
    getChainId(urls[Network.Stagenet]),
    getChainId(urls[Network.Mainnet]),
  ]).then(([testnetId, stagenetId, mainnetId]) => ({
    testnet: testnetId,
    stagenet: stagenetId,
    mainnet: mainnetId,
  }));
};
