/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
import assert from 'assert';

import { isEthereumDid } from '@arcblock/did';
import { toStakeAddress } from '@arcblock/did-util/cbor';
import { BN, fromUnitToToken, toBN } from '@ocap/util';

import { Op } from 'sequelize';
import { wallet } from '../../libs/auth';
import { events } from '../../libs/event';
import logger from '../../libs/logger';
import { Customer, GroupedBN, PaymentCurrency, PaymentMethod, Subscription } from '../../store/models';
import { fetchErc20Balance, fetchEtherBalance } from '../ethereum/token';
import { EVM_CHAIN_TYPES } from '../../libs/constants';
import { getAppStakeAddressWithFallback, getMigratedFromList } from '../../libs/wallet-migration';

export async function ensureStakedForGas() {
  const currencies = await PaymentCurrency.findAll({ where: { active: true, is_base_currency: true } });

  for (const currency of currencies) {
    const method = await PaymentMethod.findByPk(currency.payment_method_id);
    if (method && method.type === 'arcblock') {
      const client = method.getOcapClient();
      const host = method.settings.arcblock?.api_host;

      try {
        const { state: account } = await client.getAccountState({ address: wallet.address });

        // Use migration-aware fallback to check if app has staked for gas
        const stakeResult = await getAppStakeAddressWithFallback({ client });
        if (stakeResult) {
          logger.info(`app already staked for gas on chain ${host}`, { source: stakeResult.source });
          continue;
        }

        const result = await client.getForgeState({});
        const { token, txConfig } = result.state;
        const holding = (account?.tokens || []).find((x: any) => x.address === token.address);
        if (toBN(holding?.value || '0').lte(toBN(txConfig.txGas.minStake))) {
          logger.info(`not enough balance to stake for gas on chain ${host}`);
          continue;
        }

        // @ts-ignore
        const [hash] = await client.stake({
          to: wallet.address,
          message: 'stake-for-gas',
          // @ts-ignore - token.decimal may be string from DB
          tokens: [{ address: token.address, value: fromUnitToToken(txConfig.txGas.minStake, Number(token.decimal)) }],
          wallet,
        });
        logger.info(`staked for gas on chain ${host}`, { hash });
      } catch (err) {
        logger.error(`stake for gas on chain ${host} failed`, err);
      }
    }
  }
}

export async function hasStakedForGas(method: PaymentMethod, sender = wallet.address) {
  if (method.type === 'arcblock') {
    const client = method.getOcapClient();

    // If checking app's own stake, use migration-aware fallback
    if (sender === wallet.address) {
      const stakeResult = await getAppStakeAddressWithFallback({ client });
      return !!stakeResult;
    }

    // For other senders, use direct query
    const address = toStakeAddress(sender, sender);
    const { state } = await client.getStakeState({ address });
    return !!state;
  }

  return true;
}

export async function estimateMaxGasForTx(method: PaymentMethod, typeUrl = 'fg:t:delegate') {
  if (method.type === 'arcblock') {
    const client = method.getOcapClient();
    const { estimate } = await client.estimateGas({ typeUrl });
    return estimate.max;
  }

  return '0';
}

export async function getAllResults(dataKey: string, fn: Function) {
  const results = [];
  const pageSize = 40;

  const { page, [dataKey]: firstPage } = await fn({ size: pageSize });
  if (page.total < pageSize) {
    return firstPage;
  }

  results.push(...firstPage);

  const total = Math.floor(page.total / pageSize);
  const tasks = [];
  for (let i = 1; i <= total; i++) {
    tasks.push(async () => {
      const { [dataKey]: nextPage } = await fn({ size: pageSize, cursor: i * pageSize });
      results.push(...nextPage);
    });
  }
  await Promise.all(tasks.map((x) => x()));
  assert.equal(results.length, page.total, `fetched ${dataKey} count does not match`);

  return results;
}

export async function checkStakeRevokeTx() {
  const methods = await PaymentMethod.findAll({ where: { type: 'arcblock' } });
  if (methods.length === 0) {
    return;
  }

  const interval = 1000 * 60 * 10;
  const startDateTime = new Date(Date.now() - interval).toISOString();
  const endDateTime = new Date().toISOString();

  for (const method of methods) {
    const client = method.getOcapClient();
    const txs = await getAllResults('transactions', (paging: any) =>
      client.listTransactions({
        paging,
        typeFilter: { types: ['revoke_stake'] },
        timeFilter: {
          field: 'time',
          startDateTime,
          endDateTime,
        },
      })
    );

    logger.info(`Found ${txs.length} revoke stake tx on chain ${method.name}`, {
      interval,
      startDateTime,
      endDateTime,
      txs: txs.map((x: any) => x.hash),
    });

    await Promise.all(
      txs.map(async (t: any) => {
        const customer = await Customer.findByPkOrDid(t.tx.from);
        if (!customer) {
          return;
        }

        const { address } = t.tx.itxJson;

        // Check related subscriptions in the stake
        const subscriptions = await Subscription.findAll({
          where: {
            [Op.or]: [
              { 'payment_details.arcblock.staking.address': address },
              { 'overdraft_protection.payment_details.arcblock.staking.address': address },
            ],
          },
        });
        if (subscriptions.length === 0) {
          return;
        }
        logger.info(`Active subscriptions found for revoked stake on chain ${method.name}`, {
          address,
          customer: customer.did,
          subscriptions: subscriptions.map((x) => x.id),
          txHash: t.hash,
        });

        const { state: stake } = await client.getStakeState({ address });
        if (stake.nonce) {
          subscriptions
            .filter((s) => s.isActive())
            .forEach((s) => {
              events.emit('customer.stake.revoked', { subscriptionId: s.id, tx: t });
            });
        } else {
          const data = JSON.parse(stake.data?.value || '{}');
          subscriptions
            .filter((s) => s.isActive())
            .filter((s) => data[s.id])
            .forEach((s) => {
              events.emit('customer.stake.revoked', { subscriptionId: s.id, tx: t });
            });
        }
      })
    );
  }
}

export async function getStakeSummaryByDid(did: string, livemode: boolean = true): Promise<GroupedBN> {
  const methods = await PaymentMethod.findAll({
    where: { type: 'arcblock', livemode },
    include: [{ model: PaymentCurrency, as: 'payment_currencies' }],
  });
  if (methods.length === 0) {
    return {};
  }

  const results: GroupedBN = {};
  await Promise.all(
    methods.map(async (method: PaymentMethod) => {
      const client = method.getOcapClient();

      // Get all possible receiver addresses (current + migratedFrom) for migration compatibility
      const migratedFrom = await getMigratedFromList(client);
      const receiverAddresses = [...new Set([wallet.address, ...migratedFrom])];

      // Query stakes for all possible receiver addresses
      for (const receiver of receiverAddresses) {
        // eslint-disable-next-line no-await-in-loop
        const { stakes } = await client.listStakes({
          addressFilter: {
            receiver,
          },
        });
        (stakes || [])
          .filter((x: any) => x.sender === did)
          .forEach((x: any) => {
            const { tokens } = x;
            (tokens || []).forEach((t: any) => {
              // @ts-ignore
              const currency = method.payment_currencies?.find((c: PaymentCurrency) => t.address === c.contract);
              if (currency) {
                results[currency.id] = new BN(results[currency.id] || '0').add(new BN(t.balance || '0')).toString();
              }
            });
          });
      }
    })
  );

  return results;
}

export async function getTokenSummaryByDid(
  did: string,
  livemode: boolean,
  type: string | string[] = 'arcblock'
): Promise<GroupedBN> {
  const methods = await PaymentMethod.findAll({
    where: { type, livemode },
    include: [{ model: PaymentCurrency, as: 'payment_currencies' }],
  });
  if (methods.length === 0) {
    return {};
  }

  const results: GroupedBN = {};
  await Promise.all(
    methods.map(async (method: PaymentMethod) => {
      if (method.type === 'arcblock') {
        const client = method.getOcapClient();
        const { tokens } = await client.getAccountTokens({ address: did });
        (tokens || []).forEach((t: any) => {
          // @ts-ignore
          const currency = method.payment_currencies.find((c: PaymentCurrency) => t.address === c.contract);
          if (currency) {
            results[currency.id] = t.balance;
          }
        });
      }
      if (EVM_CHAIN_TYPES.includes(method.type) && isEthereumDid(did)) {
        const client = method.getEvmClient();
        await Promise.all(
          // @ts-ignore
          method.payment_currencies.map(async (c: PaymentCurrency) => {
            if (c.contract) {
              results[c.id] = await fetchErc20Balance(client, c.contract, did);
            } else {
              results[c.id] = await fetchEtherBalance(client, did);
            }
          })
        );
      }
    })
  );

  return results;
}

export async function getTokenByAddress(
  address: string,
  paymentMethod: PaymentMethod,
  paymentCurrency: PaymentCurrency
): Promise<string | undefined> {
  if (paymentMethod.type === 'arcblock') {
    const client = paymentMethod.getOcapClient();
    const { tokens } = await client.getAccountTokens({ address });
    return tokens.find((t: any) => t.address === paymentCurrency.contract)?.balance;
  }
  if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
    const client = paymentMethod.getEvmClient();
    if (paymentCurrency.contract) {
      const token = await fetchErc20Balance(client, paymentCurrency.contract, address);
      return token;
    }
    const token = await fetchEtherBalance(client, address);
    return token;
  }
  logger.info(`unsupported payment method type: ${paymentMethod.type}`, {
    paymentCurrencyId: paymentCurrency.id,
    paymentMethodId: paymentMethod.id,
    address,
  });
  return '0';
}
