/* eslint-disable @typescript-eslint/indent */
import { isEthereumDid } from '@arcblock/did';
import { getWalletDid } from '@blocklet/sdk/lib/did';
import type { DelegateState, TokenLimit } from '@ocap/client';
import { toTxHash } from '@ocap/mcrypto';
import { BN, fromUnitToToken } from '@ocap/util';
import cloneDeep from 'lodash/cloneDeep';
import type { LiteralUnion } from 'type-fest';

import { fetchErc20Allowance, fetchErc20Balance, fetchEtherBalance } from '../integrations/ethereum/token';
import {
  Invoice,
  PaymentCurrency,
  PaymentIntent,
  PaymentLink,
  PaymentMethod,
  TCustomer,
  TLineItemExpanded,
  Payout,
  CreditGrant,
  Customer,
  Subscription,
} from '../store/models';
import { getDelegationAddressWithFallback, backfillDelegationAddress, getMigratedFromList } from './wallet-migration';
import type { TPaymentCurrency } from '../store/models/payment-currency';
import { blocklet, ethWallet, wallet, getVaultAddress } from './auth';
import logger from './logger';
import {
  formatLinkWithLocale,
  getBlockletJson,
  getUserOrAppInfo,
  OCAP_PAYMENT_TX_TYPE,
  resolveAddressChainTypes,
} from './util';
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from './constants';
import { getTokenByAddress } from '../integrations/arcblock/stake';
import { isCreditMetered } from './credit-utils';

// ============================================================================
// Delegation cache - avoids redundant chain queries for the same user
// ============================================================================
const DELEGATION_CACHE_TTL_MS = 30_000; // 30 seconds

// Only cache immutable lookup results (address, source), NOT DelegateState.
// DelegateState contains on-chain token balances that change with every consumption;
// caching it could allow stale balances to pass sufficiency checks, causing overdraft.
interface DelegationCacheEntry {
  delegator: string;
  delegationAddress: string;
  source: string;
  needsBackfill: boolean;
  cachedAt: number;
}

const delegationCache = new Map<string, DelegationCacheEntry>();

function getDelegationCacheKey(userDid: string, paymentMethodId: string): string {
  return `${userDid}:${paymentMethodId}`;
}

function getCachedDelegation(key: string): DelegationCacheEntry | null {
  const entry = delegationCache.get(key);
  if (!entry) return null;
  if (Date.now() - entry.cachedAt > DELEGATION_CACHE_TTL_MS) {
    delegationCache.delete(key);
    return null;
  }
  return entry;
}

function setCachedDelegation(key: string, entry: Omit<DelegationCacheEntry, 'cachedAt'>): void {
  // Evict old entries if cache grows too large
  if (delegationCache.size > 1000) {
    const now = Date.now();
    for (const [k, v] of delegationCache) {
      if (now - v.cachedAt > DELEGATION_CACHE_TTL_MS) {
        delegationCache.delete(k);
      }
    }
  }
  delegationCache.set(key, { ...entry, cachedAt: Date.now() });
}

// Invalidate cache for a specific user (e.g., after delegation changes)
export function invalidateDelegationCache(userDid: string): void {
  for (const key of delegationCache.keys()) {
    if (key.startsWith(`${userDid}:`)) {
      delegationCache.delete(key);
    }
  }
}

export interface SufficientForPaymentResult {
  sufficient: boolean;
  reason?: LiteralUnion<
    | 'NO_DID_WALLET'
    | 'NO_DELEGATION'
    | 'NO_TRANSFER_PERMISSION'
    | 'NO_TRANSFER_TO'
    | 'NO_TOKEN_PERMISSION'
    | 'NO_TOKEN'
    | 'NO_ENOUGH_ALLOWANCE'
    | 'NO_ENOUGH_TOKEN'
    | 'NO_CUSTOMER'
    | 'NO_CREDIT_GRANTS'
    | 'NO_ENOUGH_CREDIT'
    | 'CREDIT_CHECK_ERROR'
    | 'NOT_SUPPORTED',
    string
  >;
  delegator?: string;
  state?: DelegateState;
  token?: { address: string; balance: string };
  requestedAmount?: string;
}

export async function checkTokenBalance(args: {
  paymentMethod: PaymentMethod;
  paymentCurrency: TPaymentCurrency;
  userDid: string;
  amount: string;
  skipUserCheck?: boolean;
}): Promise<SufficientForPaymentResult> {
  const { paymentMethod, paymentCurrency, userDid, amount } = args;
  const tokenAddress = paymentCurrency.contract as string;
  const totalAmount = new BN(amount);

  if (paymentMethod.type === 'arcblock') {
    // get user wallet did
    let delegator = userDid;
    if (!args.skipUserCheck) {
      const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
      if (user) {
        delegator = getWalletDid(user);
      }
    }
    if (!delegator) {
      return {
        sufficient: false,
        reason: 'NO_DID_WALLET',
        requestedAmount: totalAmount.toString(),
      };
    }

    const client = paymentMethod.getOcapClient();

    // check token balance
    const { tokens } = await client.getAccountTokens({ address: delegator, token: tokenAddress });
    const [token] = tokens;
    if (!token) {
      return {
        sufficient: false,
        reason: 'NO_TOKEN',
        requestedAmount: totalAmount.toString(),
      };
    }

    if (new BN(token.balance).lt(totalAmount)) {
      return {
        sufficient: false,
        reason: 'NO_ENOUGH_TOKEN',
        token,
        requestedAmount: totalAmount.toString(),
      };
    }

    return {
      sufficient: true,
      token,
      requestedAmount: totalAmount.toString(),
    };
  }

  if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
    if (!paymentCurrency.contract) {
      return {
        sufficient: false,
        reason: 'NO_TOKEN',
        requestedAmount: totalAmount.toString(),
      };
    }
    if (isEthereumDid(userDid) === false) {
      return {
        sufficient: false,
        reason: 'NO_TOKEN',
        requestedAmount: totalAmount.toString(),
      };
    }

    const provider = paymentMethod.getEvmClient();
    const balance = await fetchErc20Balance(provider, paymentCurrency.contract, userDid);

    if (new BN(balance).lt(totalAmount)) {
      return {
        sufficient: false,
        reason: 'NO_ENOUGH_TOKEN',
        token: { address: tokenAddress, balance },
        requestedAmount: totalAmount.toString(),
      };
    }

    return {
      sufficient: true,
      token: { address: tokenAddress, balance },
      requestedAmount: totalAmount.toString(),
    };
  }

  return {
    sufficient: true,
    requestedAmount: totalAmount.toString(),
  };
}

export async function isCreditGrantSufficientForPayment(args: {
  paymentMethod: PaymentMethod;
  paymentCurrency: TPaymentCurrency;
  userDid: string;
  amount: string;
  lineItems?: TLineItemExpanded[];
}): Promise<SufficientForPaymentResult> {
  const { paymentMethod, paymentCurrency, userDid, amount, lineItems } = args;
  if (paymentMethod.type === 'arcblock' && paymentCurrency.type === 'credit') {
    const customer = await Customer.findOne({ where: { did: userDid } });
    if (!customer) {
      return { sufficient: false, reason: 'NO_CUSTOMER' };
    }

    const priceIds: string[] = [];
    if (lineItems && lineItems.length > 0) {
      lineItems.forEach((item) => {
        if (isCreditMetered(item.price)) {
          priceIds.push(item.price_id);
        }
      });
    }

    const creditSummary = await CreditGrant.getEffectiveCreditSummary({
      customerId: customer.id,
      currencyId: paymentCurrency.id,
      priceIds,
    });
    if (!creditSummary) {
      return { sufficient: false, reason: 'NO_CREDIT_GRANTS' };
    }
    const availableCredit = new BN(creditSummary[paymentCurrency.id]?.remainingAmount || '0');
    if (availableCredit.lt(new BN(amount))) {
      return { sufficient: false, reason: 'NO_ENOUGH_CREDIT' };
    }

    return {
      sufficient: true,
      token: { address: paymentCurrency.id, balance: availableCredit.toString() },
      requestedAmount: amount,
    };
  }
  return {
    sufficient: false,
    reason: 'NOT_SUPPORTED',
  };
}

export async function isDelegationSufficientForPayment(args: {
  paymentMethod: PaymentMethod;
  paymentCurrency: TPaymentCurrency;
  userDid: string;
  amount: string;
  delegatorAmounts?: string[];
  lineItems?: TLineItemExpanded[];
  subscription?: Subscription | null; // Optional subscription for migration fallback
  storedDelegationAddress?: string; // Direct stored address (for auto-recharge etc.)
}): Promise<SufficientForPaymentResult> {
  const { paymentCurrency, paymentMethod, userDid, amount, delegatorAmounts, subscription, storedDelegationAddress } =
    args;
  if (!userDid) {
    return { sufficient: false, reason: 'NO_DID_WALLET' };
  }
  const tokenAddress = paymentCurrency.contract as string;

  let totalAmount = new BN(amount);
  const hasDelegatorAmounts = delegatorAmounts && delegatorAmounts.length > 0;
  if (hasDelegatorAmounts) {
    totalAmount = delegatorAmounts.reduce((sum, amt) => sum.add(new BN(amt)), new BN('0'));
  }

  if (paymentMethod.type === 'arcblock') {
    // Check if this is a credit currency type
    if (paymentCurrency.type === 'credit') {
      const result = await isCreditGrantSufficientForPayment({
        paymentMethod,
        paymentCurrency,
        userDid,
        amount,
        lineItems: args.lineItems,
      });
      return result;
    }

    // Regular token handling for non-credit currencies
    // user have bond wallet did?
    let delegator = userDid;
    const { user } = await blocklet.getUser(userDid, { enableConnectedAccount: true });
    if (user) {
      delegator = getWalletDid(user);
      if (!delegator) {
        return { sufficient: false, reason: 'NO_DID_WALLET' };
      }
    }

    // Log delegation check context for debugging
    logger.info('isDelegationSufficientForPayment: checking delegation', {
      originalUserDid: userDid,
      resolvedDelegator: delegator,
      delegatorChanged: userDid !== delegator,
      walletAddress: wallet.address,
      paymentMethodApiHost: paymentMethod.settings?.arcblock?.api_host,
    });

    const client = paymentMethod.getOcapClient();
    const cacheKey = getDelegationCacheKey(delegator, paymentMethod.id);
    const cached = getCachedDelegation(cacheKey);

    let delegationAddress: string;
    let source: string;

    if (cached) {
      // Use cached delegation address (immutable), but always query fresh state
      delegationAddress = cached.delegationAddress;
      source = cached.source;
      logger.info('isDelegationSufficientForPayment: using cached delegation address', {
        delegator,
        delegationAddress,
        source,
        cacheAgeMs: Date.now() - cached.cachedAt,
      });
    } else {
      // have delegated before? Use migration-aware fallback query
      // Priority: storedDelegationAddress > subscription.payment_details.arcblock.delegation_address
      const delegationResult = await getDelegationAddressWithFallback({
        storedAddress: storedDelegationAddress || subscription?.payment_details?.arcblock?.delegation_address,
        delegator,
        client,
      });
      if (!delegationResult) {
        logger.error('isDelegationSufficientForPayment: no delegation address found', {
          delegator,
          storedDelegationAddress,
          subscriptionDelegationAddress: subscription?.payment_details?.arcblock?.delegation_address,
          subscriptionId: subscription?.id,
        });
        return { sufficient: false, reason: 'NO_DELEGATION' };
      }

      const { address, needsBackfill } = delegationResult;
      delegationAddress = address;
      source = delegationResult.source;

      // Backfill if needed and subscription is available
      if (needsBackfill && subscription) {
        await backfillDelegationAddress(subscription.id, address);
      }

      // Cache the immutable lookup result (address + source), not the state
      setCachedDelegation(cacheKey, {
        delegator,
        delegationAddress: address,
        source,
        needsBackfill,
      });
    }

    // Always query fresh DelegateState — balances change with every consumption
    const delegateResult = await client.getDelegateState({ address: delegationAddress });
    if (!delegateResult.state) {
      logger.error('isDelegationSufficientForPayment: no delegation state', {
        address: delegationAddress,
        delegator,
        source,
      });
      return { sufficient: false, reason: 'NO_DELEGATION' };
    }
    const { state } = delegateResult;

    if (!state.ops || state.ops?.length === 0) {
      logger.error('isDelegationSufficientForPayment: no delegation ops', {
        address: delegationAddress,
        delegator,
        source,
      });
      return { sufficient: false, reason: 'NO_DELEGATION', state };
    }

    // have transfer permissions?
    const grant = (state as DelegateState).ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE)?.value;
    if (!grant) {
      return { sufficient: false, reason: 'NO_TRANSFER_PERMISSION', state };
    }

    // check token limits
    if (grant.limit && Array.isArray(grant.limit.tokens) && grant.limit.tokens.length > 0) {
      const tokenLimit = grant.limit.tokens.find((x: any) => x.address === tokenAddress);
      if (!tokenLimit) {
        return { sufficient: false, reason: 'NO_TOKEN_PERMISSION', state };
      }

      // FIXME: @wangshijun check other conditions in the token limit: txCount, totalAllowance, validUntil, rateLimit

      // Check if tokenLimit.to includes current wallet.address or any migratedFrom addresses
      if (Array.isArray(tokenLimit.to) && tokenLimit.to.length) {
        const migratedFrom = await getMigratedFromList(client);
        const allWalletAddresses = [wallet.address, ...migratedFrom];
        const hasValidTo = allWalletAddresses.some((addr) => tokenLimit.to.includes(addr));
        if (!hasValidTo) {
          return { sufficient: false, reason: 'NO_TRANSFER_TO', state };
        }
      }

      // check allowance
      if (tokenLimit.txAllowance !== '0') {
        const allowance = new BN(tokenLimit.txAllowance);
        if (hasDelegatorAmounts) {
          // check each delegator amount
          const invalidAmount = delegatorAmounts.find((amt) => new BN(amt).gt(allowance));

          if (invalidAmount) {
            return {
              sufficient: false,
              reason: 'NO_ENOUGH_ALLOWANCE',
              state,
            };
          }
        } else if (totalAmount.gt(allowance)) {
          return {
            sufficient: false,
            reason: 'NO_ENOUGH_ALLOWANCE',
            state,
          };
        }
      }
    }

    // balance enough token for payment?
    const { tokens } = await client.getAccountTokens({ address: delegator, token: tokenAddress });
    const [token] = tokens;
    if (!token) {
      return { sufficient: false, reason: 'NO_TOKEN', state };
    }

    if (new BN(token.balance).lt(totalAmount)) {
      return {
        sufficient: false,
        reason: 'NO_ENOUGH_TOKEN',
        token,
        state,
        requestedAmount: totalAmount.toString(),
      };
    }

    return {
      sufficient: true,
      delegator,
      state,
      token,
      requestedAmount: totalAmount.toString(),
    };
  }

  if (paymentMethod.type === 'stripe') {
    return { sufficient: false, reason: 'NOT_SUPPORTED' };
  }

  if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
    if (!paymentCurrency.contract) {
      return { sufficient: false, reason: 'NOT_SUPPORTED' };
    }
    if (isEthereumDid(userDid) === false) {
      return { sufficient: false, reason: 'NOT_SUPPORTED' };
    }

    const provider = paymentMethod.getEvmClient();
    const balance = await fetchErc20Balance(provider, paymentCurrency.contract, userDid);
    if (new BN(balance).lt(new BN(amount))) {
      return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
    }

    const allowance = await fetchErc20Allowance(provider, paymentCurrency.contract, userDid, ethWallet.address);
    logger.info('isDelegationSufficientForPayment', { userDid, amount, allowance, contract: paymentCurrency.contract });
    if (new BN(allowance).lt(new BN(amount))) {
      return { sufficient: false, reason: 'NO_ENOUGH_ALLOWANCE' };
    }

    return { sufficient: true };
  }

  throw new Error(`Payment method ${paymentMethod.type} not supported`);
}

export function isCreditSufficientForPayment(args: {
  paymentMethod: PaymentMethod;
  paymentCurrency: TPaymentCurrency;
  customer: TCustomer;
  amount: string;
}): { sufficient: boolean; balance: string; reason?: string } {
  const { paymentMethod, paymentCurrency, customer, amount } = args;
  if (paymentMethod.type === 'stripe') {
    return { sufficient: false, balance: customer.balance || '0', reason: 'NOT_SUPPORTED' };
  }

  const tokens = customer.token_balance || {};
  const balance = tokens[paymentCurrency.id] || '0';

  if (amount === '0') {
    return { sufficient: true, balance };
  }

  if (new BN(balance).lt(new BN(amount))) {
    return { sufficient: false, balance, reason: 'NO_ENOUGH_TOKEN' };
  }

  return { sufficient: true, balance };
}

export async function getGasPayerExtra(txBuffer: Buffer, headers?: { [key: string]: string }) {
  if (headers && headers['x-gas-payer-sig'] && headers['x-gas-payer-pk']) {
    return { headers };
  }

  const txHash = toTxHash(txBuffer);
  return {
    headers: {
      'x-gas-payer-sig': await wallet.signJWT({ txHash }),
      'x-gas-payer-pk': wallet.publicKey,
    },
  };
}

export interface PaymentDetail {
  balance: number;
  price: number;
  symbol: LiteralUnion<'ABT' | 'USD', string>;
  balanceFormatted?: string;
}
export async function getPaymentDetail(
  userDid: string,
  invoice: Invoice,
  amount: string = '0'
): Promise<PaymentDetail> {
  const defaultResult = {
    balance: 0,
    price: 0,
    symbol: '',
  };

  const paymentCurrency = await PaymentCurrency.findByPk(invoice.currency_id);
  if (!paymentCurrency) {
    return defaultResult;
  }
  Object.assign(defaultResult, { symbol: paymentCurrency.symbol });

  const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
  const inputAmount: string = paymentIntent ? paymentIntent.amount : amount;

  const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
  if (!paymentMethod) {
    return defaultResult;
  }
  const paymentSettings = invoice?.payment_settings;
  if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
    // balance enough token for payment?
    const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;
    const result = await isDelegationSufficientForPayment({
      paymentMethod,
      paymentCurrency,
      userDid: payer || userDid,
      amount: inputAmount,
    });

    // Do not have enough permission
    if (!result.token) {
      return defaultResult;
    }

    // Do not have enough token
    const price: number = +fromUnitToToken(inputAmount, paymentCurrency.decimal);
    if (!result.delegator) {
      return {
        balance: 0,
        price,
        symbol: paymentCurrency.symbol,
      };
    }

    return {
      balance: +fromUnitToToken(result.token.balance, paymentCurrency.decimal),
      price,
      symbol: paymentCurrency.symbol,
    };
  }

  return defaultResult;
}

export async function getTokenLimitsForDelegation(
  items: TLineItemExpanded[],
  paymentMethod: PaymentMethod,
  paymentCurrency: PaymentCurrency,
  address: string,
  amount: string,
  // Optional: if caller already fetched DelegateState (e.g. isDelegationSufficientForPayment),
  // pass it here to skip a redundant chain RPC. Caller MUST use state from the same request
  // to avoid stale-balance issues (see note on delegationCache above).
  delegateState?: DelegateState | null
): Promise<TokenLimit[]> {
  const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
  const allowance = hasMetered ? '0' : amount;

  // @ts-ignore
  const entry: TokenLimit = {
    address: paymentCurrency.contract as string,
    to: [wallet.address], // FIXME: may broken if we have vault, migrated
    txAllowance: allowance,
    totalAllowance: '0',
    txCount: 0,
    validUntil: 0,
  };

  if (paymentMethod.type === 'arcblock') {
    let state: DelegateState | null | undefined = delegateState;
    if (state === undefined) {
      const client = paymentMethod.getOcapClient();
      // CF Workers: warm up chain context to avoid setTimeout(resolve,0) hang on cold client.
      await client.getContext();
      ({ state } = await client.getDelegateState({ address }));
      logger.info('getTokenLimitsForDelegation: fetched DelegateState from chain', { address });
    } else {
      logger.info('getTokenLimitsForDelegation: reused DelegateState from caller', { address });
    }

    // If we never delegated before
    if (!state) {
      return [entry];
    }

    if (!state.ops || state.ops?.length === 0) {
      return [entry];
    }

    const op = state.ops.find((x: any) => x.key === OCAP_PAYMENT_TX_TYPE);
    if (op && Array.isArray(op.value.limit?.tokens) && op.value.limit.tokens.length > 0) {
      const tokenLimits = cloneDeep(op.value.limit.tokens);
      const index = op.value.limit.tokens.findIndex((x: any) => x.address === paymentCurrency.contract);
      // we are updating an existing token limit
      if (index > -1) {
        const limit = op.value.limit.tokens[index] as TokenLimit;
        // If we have a previous delegation and the txAllowance is smaller than requested amount
        // If txAllowance is 0 (unlimited), we should not update it
        if (limit.txAllowance !== '0' && new BN(limit.txAllowance).lt(new BN(amount))) {
          tokenLimits[index] = entry;
        }
      } else {
        // we are adding a new token limit
        tokenLimits.push(entry);
      }

      return tokenLimits;
    }

    return [entry];
  }

  if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
    // FIXME: @wangshijun better control from customer.token_limits
    entry.totalAllowance = new BN(amount).mul(new BN(12)).toString();
    return [entry];
  }

  throw new Error(`getTokenLimitsForDelegation: Payment method ${paymentMethod.type} not supported`);
}

export async function isBalanceSufficientForRefund(args: {
  paymentMethod: PaymentMethod;
  paymentCurrency: TPaymentCurrency;
  amount: string;
}): Promise<SufficientForPaymentResult> {
  const { paymentCurrency, paymentMethod, amount } = args;
  const tokenAddress = paymentCurrency.contract as string;

  if (paymentMethod.type === 'arcblock') {
    const client = paymentMethod.getOcapClient();
    const { tokens } = await client.getAccountTokens({ address: wallet.address, token: tokenAddress });
    const [token] = tokens;
    if (!token) {
      return { sufficient: false, reason: 'NO_TOKEN' };
    }
    if (new BN(token.balance).lt(new BN(amount))) {
      return { sufficient: false, reason: 'NO_ENOUGH_TOKEN', token };
    }

    return { sufficient: true, token };
  }

  if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
    const provider = paymentMethod.getEvmClient();
    const balance = paymentCurrency.contract
      ? await fetchErc20Balance(provider, paymentCurrency.contract, ethWallet.address)
      : await fetchEtherBalance(provider, ethWallet.address);
    if (new BN(balance).lt(new BN(amount))) {
      return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
    }

    return { sufficient: true };
  }

  if (paymentMethod.type === 'stripe') {
    return { sufficient: false, reason: 'NOT_SUPPORTED' };
  }

  throw new Error(`isBalanceSufficientForRefund: Payment method ${paymentMethod.type} not supported`);
}

export async function getDonationBenefits(paymentLink: PaymentLink, url?: string, locale?: string) {
  const { donation_settings: donationSettings } = paymentLink;
  if (!donationSettings) {
    return [];
  }
  const { beneficiaries } = donationSettings;
  const total = beneficiaries.reduce((t, x) => t + Number(x.share), 0);
  if (total === 0) {
    return beneficiaries;
  }
  const blockletJson = await getBlockletJson(url);
  const result = await Promise.all(
    beneficiaries.map(async (beneficiary) => {
      const { address, share, name, avatar } = beneficiary;
      try {
        const info = await getUserOrAppInfo(address, blockletJson);
        return {
          address,
          percent: (Number(share) * 100) / total,
          name: name || info?.name || '',
          avatar: avatar || info?.avatar || '',
          url: formatLinkWithLocale(info?.url || '', locale),
          type: info?.type || 'user',
        };
      } catch (error) {
        return {
          address,
          percent: (Number(share) * 100) / total,
          name: name || '',
          avatar: avatar || '',
        };
      }
    })
  );
  return result;
}

/**
 * 检查是否需要向冷钱包转账及可转账金额
 * @param paymentCurrencyId
 * @returns {Promise<{depositAmount: string, message?: string, vaultAddress?: string, paymentMethod?: PaymentMethod, paymentCurrency?: PaymentCurrency}>}
 */
export async function checkDepositVaultAmount(paymentCurrencyId: string): Promise<{
  depositAmount: string;
  message?: string;
  vaultAddress?: string;
  paymentCurrency?: PaymentCurrency;
}> {
  const paymentCurrency = await PaymentCurrency.scope('withVaultConfig').findByPk(paymentCurrencyId);
  if (!paymentCurrency) {
    return { depositAmount: '0', message: 'Payment currency not found' };
  }

  if (!paymentCurrency?.vault_config?.enabled) {
    return { depositAmount: '0', message: 'Deposit vault is not enabled' };
  }

  const depositThreshold = paymentCurrency?.vault_config?.deposit_threshold;
  if (!depositThreshold || depositThreshold === '0') {
    return { depositAmount: '0', message: 'Deposit threshold is not set or zero' };
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    return { depositAmount: '0', message: 'Payment method not found' };
  }

  const vaultAddress = await getVaultAddress();
  if (!vaultAddress) {
    return { depositAmount: '0', message: 'Vault address is not found' };
  }

  const vaultChainTypes = resolveAddressChainTypes(vaultAddress);
  if (!vaultChainTypes.includes(paymentMethod.type)) {
    return { depositAmount: '0', message: 'Vault chain type is not supported' };
  }

  const walletAddress = paymentMethod.type === 'arcblock' ? wallet.address : ethWallet.address;
  const balance = await getTokenByAddress(walletAddress, paymentMethod, paymentCurrency);

  const depositThresholdBN = new BN(depositThreshold);
  if (new BN(balance).lte(depositThresholdBN)) {
    return { depositAmount: '0', message: 'No enough balance to deposit to vault' };
  }

  const balanceBN = new BN(balance);
  let amountToDeposit = balanceBN.sub(depositThresholdBN).toString();

  const { [paymentCurrency.id]: lockedAmount } = await Payout.getPayoutLockedAmount({
    currency_id: paymentCurrency.id,
  });

  // check if the amount to payout is already greater than the deposit threshold
  if (new BN(lockedAmount).add(depositThresholdBN).gte(balanceBN)) {
    return { depositAmount: '0', message: 'Amount to payout is already greater than the deposit threshold' };
  }

  amountToDeposit = new BN(amountToDeposit).sub(new BN(lockedAmount)).toString();

  if (new BN(amountToDeposit).lte(new BN(0))) {
    return { depositAmount: '0', message: 'No amount available to deposit after calculations' };
  }

  const bufferThreshold = paymentCurrency?.vault_config?.buffer_threshold;
  if (bufferThreshold && bufferThreshold !== '0' && new BN(amountToDeposit).lt(new BN(bufferThreshold))) {
    return { depositAmount: '0', message: 'Amount to deposit is less than the buffer threshold' };
  }

  return {
    depositAmount: amountToDeposit,
    vaultAddress,
    paymentCurrency,
  };
}
