import type { Transaction } from '@ocap/client';
import { toBase58 } from '@ocap/util';
import { fromAddress } from '@ocap/wallet';

import { encodeTransferItx } from '../../integrations/ethereum/token';
import { waitForEvmTxConfirm, waitForEvmTxReceipt } from '../../integrations/ethereum/tx';
import type { CallbackArgs } from '../../libs/auth';
import { ethWallet, wallet } from '../../libs/auth';
import logger from '../../libs/logger';
import { getGasPayerExtra } from '../../libs/payment';
import { onSubscriptionUpdateConnected } from '../../libs/subscription';
import { getTxMetadata } from '../../libs/util';
import { invoiceQueue } from '../../queues/invoice';
import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
import { EVMChainType, Price, Subscription, SubscriptionItem } from '../../store/models';
import { ensureInvoiceForCollect, getAuthPrincipalClaim, getDelegationTxClaim } from './shared';
import { EVM_CHAIN_TYPES } from '../../libs/constants';

// Used to collect an open invoice failed to collect automatically
export default {
  action: 'collect',
  authPrincipal: false,
  claims: {
    authPrincipal: async ({ extraParams }: CallbackArgs) => {
      const { allowCreatePI } = extraParams;
      const { paymentMethod } = await ensureInvoiceForCollect(extraParams.invoiceId, { allowCreatePI });
      return getAuthPrincipalClaim(paymentMethod, 'pay');
    },
  },
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
    const { invoiceId, action, allowCreatePI } = extraParams;
    const { invoice, paymentIntent, paymentCurrency, paymentMethod } = await ensureInvoiceForCollect(invoiceId, {
      allowCreatePI,
    });

    if (paymentMethod.type === 'arcblock') {
      const tokens = [{ address: paymentCurrency.contract as string, value: invoice.amount_due }];
      // @ts-ignore
      const itx: TransferV3Tx = {
        outputs: [{ owner: wallet.address, tokens, assets: [] }],
        data: getTxMetadata({
          paymentIntentId: paymentIntent.id,
          invoiceId,
        }),
      };

      const claims: { [key: string]: object } = {
        prepareTx: {
          type: 'TransferV3Tx',
          description: `Pay for invoice ${invoice.number}`,
          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,
          },
        },
      };

      if (action === 'renew' && invoice.subscription_id) {
        const subscription = await Subscription.findByPk(invoice.subscription_id);
        const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription!.id } });
        const items = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
        claims.signature = await getDelegationTxClaim({
          mode: 'subscription',
          userDid,
          userPk,
          nonce: invoice.id,
          data: getTxMetadata({ subscriptionId: subscription!.id, invoiceId: invoice.id }),
          trialing: false,
          paymentCurrency,
          paymentMethod,
          items,
          requiredStake: false,
          slippageConfig: subscription?.slippage_config || undefined,
        });
      }

      return claims;
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      // FIXME: @wangshijun can we support this
      if (action === 'renew' && invoice.subscription_id) {
        throw new Error(`Renew subscription on collect payment not supported for ${paymentMethod.type}`);
      }

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

    throw new Error(`Payment method ${paymentMethod.type} not supported`);
  },
  onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
    const { invoiceId, allowCreatePI } = extraParams;
    const { invoice, paymentIntent, paymentMethod } = await ensureInvoiceForCollect(invoiceId, { allowCreatePI });
    const afterTxExecution = async (paymentDetails: any) => {
      await paymentIntent.update({
        status: 'succeeded',
        amount_received: invoice.amount_due,
        capture_method: 'manual',
        last_payment_error: null,
        payment_details: {
          [paymentMethod.type]: paymentDetails,
        },
      });

      await handlePaymentSucceed(paymentIntent);

      // cleanup the queue
      let exist = await paymentQueue.get(paymentIntent.id);
      if (exist) {
        await paymentQueue.delete(paymentIntent.id);
      }
      exist = await invoiceQueue.get(invoice.id);
      if (exist) {
        await invoiceQueue.delete(invoice.id);
      }

      if (invoice.subscription_id && invoice.billing_reason === 'subscription_update') {
        const subscription = await Subscription.findByPk(invoice.subscription_id);
        if (subscription?.pending_update?.updates?.latest_invoice_id === invoice.id) {
          logger.info('Try to finalize subscription update on invoice paid', {
            invoice: invoice.id,
            subscription: subscription.id,
          });
          await onSubscriptionUpdateConnected(invoice.subscription_id);
        }
      }
    };

    if (paymentMethod.type === 'arcblock') {
      try {
        await paymentIntent.update({ status: 'processing' });
        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) {
        logger.error('Failed to finalize collect on arcblock', { paymentIntent: paymentIntent.id, error: err });
        await paymentIntent.update({ status: 'requires_capture' });
        throw err;
      }
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      await paymentIntent.update({ status: 'processing' });
      const client = paymentMethod.getEvmClient();
      const claim = claims.find((x) => x.type === 'signature');

      const receipt = await waitForEvmTxReceipt(client, claim.hash);
      if (!receipt.status) {
        throw new Error(`EVM Transaction failed: ${claim.hash}`);
      }

      waitForEvmTxConfirm(client, Number(receipt.blockNumber), paymentMethod.confirmation.block)
        .then(async () => {
          await afterTxExecution({
            tx_hash: claim.hash,
            block_height: receipt.blockNumber.toString(),
            gas_used: receipt.gasUsed.toString(),
            gas_price: receipt.gasPrice.toString(),
            payer: userDid,
            type: 'transfer',
          });
        })
        .catch(console.error);

      return { hash: claim.hash };
    }

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