import { successResponse } from '../utils';
import {
  BalancePayload,
  CreateWalletPayload,
  GenerateWalletFromMnemonicPayload,
  GetAddressFromPrivateKeyPayload,
  GetTransactionPayload,
  IGetTokenInfoPayload,
  IResponse,
  ISmartContractCallPayload,
  ITokenInfo,
  TransferPayload,
} from '../utils/types';
import {
  MAIN_NET_CHAIN_ID,
  TEST_NET_CHAIN_ID,
  address,
  privateKey,
  randomSeed,
} from '@waves/ts-lib-crypto';
import {
  nodeInteraction,
  transfer as transferAsset,
  invokeScript,
  IInvokeScriptParams,
  ITransferParams,
} from '@waves/waves-transactions';
import axios from 'axios';

const WAVES_DECIMALS = 8;

const createWallet = ({ cluster }: CreateWalletPayload): IResponse => {
  const seed = randomSeed();
  const chainId = getChainIdWithCluster(cluster);

  return successResponse({
    address: address(seed, chainId),
    privateKey: privateKey(seed),
    mnemonic: seed,
  });
};

const generateWalletFromMnemonic = ({
  mnemonic,
  cluster,
}: GenerateWalletFromMnemonicPayload): IResponse => {
  const chainId = getChainIdWithCluster(cluster);

  return successResponse({
    address: address(mnemonic, chainId),
    privateKey: privateKey(mnemonic),
    mnemonic: mnemonic,
  });
};

const getAddressFromPrivateKey = ({
  privateKey,
}: GetAddressFromPrivateKeyPayload): IResponse => {
  const chainId = getChainIdWithAddress(privateKey);

  return successResponse({
    address: address(privateKey, chainId),
  });
};

const getBalance = async (args: BalancePayload): Promise<IResponse> => {
  try {
    if (!args.rpcUrl) {
      throw new Error('Error: Node URL is required');
    }

    if (args.tokenAddress) {
      const balance = await nodeInteraction.assetBalance(
        args.tokenAddress,
        args.address,
        args.rpcUrl
      );

      const tokenInfo = await getTokenInfo({
        address: args.tokenAddress,
        rpcUrl: args.rpcUrl,
        network: args.network,
      });

      return successResponse({
        balance: Number(balance) / Math.pow(10, tokenInfo.decimals),
      });
    }

    const balance = await nodeInteraction.balance(args.address, args.rpcUrl);

    return successResponse({
      balance: balance / Math.pow(10, WAVES_DECIMALS),
    });
  } catch (error) {
    throw error;
  }
};

const transfer = async (args: TransferPayload): Promise<IResponse> => {
  try {
    if (!args.rpcUrl) {
      throw new Error('Error: Node URL is required');
    }

    let amount;
    if (args.tokenAddress) {
      const tokenInfo = await getTokenInfo({
        address: args.tokenAddress,
        rpcUrl: args.rpcUrl,
        network: args.network,
      });

      amount = args.amount * Math.pow(10, tokenInfo.decimals);
    } else {
      amount = args.amount * Math.pow(10, WAVES_DECIMALS);
    }

    const params = {
      assetId: args.tokenAddress,
      recipient: args.recipientAddress,
      amount: parseInt(String(amount)),
      chainId: getChainIdWithAddress(args.recipientAddress),
    } as ITransferParams;

    const signedTx = transferAsset(params, args.privateKey);
    const broadcastedTx = await nodeInteraction.broadcast(
      signedTx,
      args.rpcUrl
    );

    return successResponse({
      ...broadcastedTx,
    });
  } catch (error) {
    throw error;
  }
};

const getTransaction = async (
  args: GetTransactionPayload
): Promise<IResponse> => {
  try {
    if (!args.rpcUrl) {
      throw new Error('Error: Node URL is required');
    }

    const tx = await nodeInteraction.transactionById(args.hash, args.rpcUrl);

    return successResponse({
      ...tx,
    });
  } catch (error) {
    throw error;
  }
};

const getTokenInfo = async (args: IGetTokenInfoPayload): Promise<IResponse> => {
  try {
    const url = new URL(`assets/details/${args.address}`, args.rpcUrl);
    const { data } = await axios.get(url.toString());

    const info: ITokenInfo = {
      name: data.name,
      symbol: data.name,
      address: data.assetId,
      decimals: data.decimals,
      totalSupply: data.quantity.toString(),
    };

    return successResponse({ ...info });
  } catch (error) {
    throw error;
  }
};

const smartContractCall = async (
  args: ISmartContractCallPayload
): Promise<IResponse> => {
  let data;

  if (args.methodType === 'write') {
    const params = {
      dApp: args.contractAddress,
      call: {
        function: args.method,
        args: [...(args.params || [])],
      },
      payment: [...(args.payment || [])],
      chainId: getChainIdWithAddress(args.contractAddress),
    } as IInvokeScriptParams;

    const signedTx = invokeScript(params, args.privateKey!);
    data = await nodeInteraction.broadcast(signedTx, args.rpcUrl);
  } else if (args.methodType === 'read') {
    const response = await nodeInteraction.accountDataByKey(
      args.method,
      args.contractAddress,
      args.rpcUrl
    );

    data = response?.value;
  }

  return successResponse({
    data,
  });
};

function getChainIdWithCluster(cluster?: string) {
  return cluster === 'testnet'
    ? TEST_NET_CHAIN_ID
    : cluster === 'mainnet'
    ? MAIN_NET_CHAIN_ID
    : undefined;
}

function getChainIdWithAddress(address: string) {
  return address.startsWith('3P') ? MAIN_NET_CHAIN_ID : TEST_NET_CHAIN_ID;
}

export default {
  getBalance,
  createWallet,
  generateWalletFromMnemonic,
  getAddressFromPrivateKey,
  transfer,
  getTransaction,
  getTokenInfo,
  smartContractCall,
};
