import type { Transaction } from '@ocap/client';
import { fromAddress } from '@ocap/wallet';
import { fromTokenToUnit, toBase58 } from '@ocap/util';
import pAll from 'p-all';
import { encodeTransferItx } from '../../integrations/ethereum/token';
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
import type { CallbackArgs } from '../../libs/auth';
import { getGasPayerExtra } from '../../libs/payment';
import { getTxMetadata } from '../../libs/util';
import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
import logger from '../../libs/logger';
import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
import { Customer, EVMChainType, PaymentCurrency } from '../../store/models';
import { EVM_CHAIN_TYPES } from '../../libs/constants';
import { checkAndTriggerAutoRecharge } from '../../queues/auto-recharge';
import { getCustomerCreditBalance } from '../../libs/credit-grant';

async function triggerAutoRecharge(customer: Customer) {
  try {
    const currencies = await PaymentCurrency.findAll({
      where: {
        type: 'credit',
        active: true,
      },
    });
    if (currencies.length === 0) {
      return;
    }
    await pAll(
      currencies.map((currency) => async () => {
        const balanceInfo = await getCustomerCreditBalance(customer.id, currency.id);
        await checkAndTriggerAutoRecharge(customer, currency.id, balanceInfo.totalBalance);
      }),
      { concurrency: 5, stopOnError: false }
    );
  } catch (err) {
    logger.error('Failed to check and trigger auto recharge', {
      error: err,
      customerId: customer.id,
    });
  }
}
export default {
  action: 'recharge-account',
  authPrincipal: false,
  claims: {
    authPrincipal: async ({ extraParams }: CallbackArgs) => {
      const { paymentMethod } = await ensureAccountRecharge(
        extraParams.customerDid,
        extraParams.currencyId,
        extraParams.rechargeAddress
      );
      return getAuthPrincipalClaim(paymentMethod, 'recharge-account');
    },
  },
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
    const { customerDid, currencyId, rechargeAddress } = extraParams;
    let { amount } = extraParams;
    const { paymentMethod, paymentCurrency, customer, receiverAddress } = await ensureAccountRecharge(
      customerDid,
      currencyId,
      rechargeAddress
    );
    amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
    if (paymentMethod.type === 'arcblock') {
      const tokens = [{ address: paymentCurrency.contract as string, value: amount }];
      // @ts-ignore
      const itx: TransferV3Tx = {
        outputs: [{ owner: receiverAddress, tokens, assets: [] }],
        data: getTxMetadata({
          rechargeCustomerId: customer.id,
          customerDid,
          receiverAddress,
          currencyId: paymentCurrency.id,
          amount,
          action: 'recharge-account',
        }),
      };

      const claims: { [key: string]: object } = {
        prepareTx: {
          type: 'TransferV3Tx',
          description: `Recharge account for ${receiverAddress}`,
          partialTx: { from: userDid, pk: userPk, itx },
          requirement: { tokens },
          chainInfo: {
            host: paymentMethod.settings?.arcblock?.api_host as string,
            id: paymentMethod.settings?.arcblock?.chain_id as string,
          },
        },
      };

      return claims;
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      return {
        signature: {
          type: 'eth:transaction',
          data: toBase58(
            Buffer.from(
              JSON.stringify({
                network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
                tx: encodeTransferItx(receiverAddress, amount, paymentCurrency.contract as string),
              }),
              'utf-8'
            )
          ),
        },
      };
    }

    throw new Error(`Payment method ${paymentMethod.type} not supported`);
  },
  onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
    const { customerDid, currencyId, rechargeAddress } = extraParams;
    const { paymentMethod, paymentCurrency, customer, receiverAddress } = await ensureAccountRecharge(
      customerDid,
      currencyId,
      rechargeAddress
    );
    let { amount } = extraParams;
    amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();

    const afterTxExecution = async (paymentDetails: any) => {
      await ensureRechargeInvoice(
        {
          total: amount,
          description: 'Add funds for account',
          currency_id: paymentCurrency.id,
          metadata: {
            payment_details: {
              [paymentMethod.type]: paymentDetails,
              receiverAddress,
            },
          },
          livemode: paymentCurrency?.livemode,
          payment_settings: {
            payment_method_types: [paymentMethod.type],
            payment_method_options: {
              [paymentMethod.type]: {
                payer: userDid,
              },
            },
          },
        },
        null,
        paymentMethod,
        customer
      );
      try {
        retryUncollectibleInvoices({
          customerId: customer.id,
          currencyId: paymentCurrency.id,
        });
      } catch (err) {
        logger.error('Failed to retry uncollectible invoices', {
          error: err,
          customerId: customer.id,
          currencyId: paymentCurrency.id,
        });
      }

      try {
        await retryUncollectibleInvoices({
          customerId: customer.id,
          currencyId: paymentCurrency.id,
          billingReason: 'auto_recharge',
          statusList: ['open'],
        });
      } catch (err) {
        logger.error('Failed to retry auto recharge invoices', {
          error: err,
          customerId: customer.id,
          currencyId: paymentCurrency.id,
        });
      }

      try {
        await triggerAutoRecharge(customer);
      } catch (err) {
        logger.error('Failed to check and trigger auto recharge', {
          error: err,
          customerId: customer.id,
        });
      }
    };
    if (paymentMethod.type === 'arcblock') {
      try {
        const client = paymentMethod.getOcapClient();
        const claim = claims.find((x) => x.type === 'prepareTx');

        const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
        if (claim.delegator && claim.from) {
          tx.delegator = claim.delegator;
          tx.from = claim.from;
        }

        // @ts-ignore
        const { buffer } = await client.encodeTransferV3Tx({ tx });
        const txHash = await client.sendTransferV3Tx(
          // @ts-ignore
          { tx, wallet: fromAddress(userDid) },
          await getGasPayerExtra(buffer, client.pickGasPayerHeaders(request))
        );

        await afterTxExecution({
          tx_hash: txHash,
          payer: userDid,
          type: 'transfer',
        });
        return { hash: txHash };
      } catch (err) {
        console.error(err);
        logger.error('Failed to finalize recharge on arcblock', {
          receiverAddress,
          customerId: customer.id,
          error: err,
        });
        throw err;
      }
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      try {
        const paymentDetails = await executeEvmTransaction('transfer', userDid, claims, paymentMethod);
        waitForEvmTxConfirm(
          paymentMethod.getEvmClient(),
          Number(paymentDetails.block_height),
          paymentMethod.confirmation.block
        )
          .then(async () => {
            await afterTxExecution(paymentDetails);
          })
          .catch(console.error);

        return { hash: paymentDetails.tx_hash };
      } catch (err) {
        console.error(err);
        logger.error(`Failed to finalize recharge on ${paymentMethod.type}`, {
          receiverAddress,
          error: err,
        });
        throw err;
      }
    }

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