/* eslint-disable @typescript-eslint/indent */
/* eslint-disable prettier/prettier */
import { toTypeInfo } from '@arcblock/did';
import { toDelegateAddress } from '@arcblock/did-util';
import type { Transaction } from '@ocap/client';
import { BN, fromTokenToUnit, toBase58 } from '@ocap/util';
import { fromPublicKey } from '@ocap/wallet';
import type { Request } from 'express';
import { isEmpty } from 'lodash';
import dayjs from '../../libs/dayjs';
import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/arcblock/stake';
import { encodeApproveItx } from '../../integrations/ethereum/token';
import { blocklet, ethWallet, wallet } from '../../libs/auth';
import logger from '../../libs/logger';
import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
import {
  getCheckoutAmount,
  getCheckoutSessionSubscriptionIds,
  getFastCheckoutAmount,
  getStatementDescriptor,
  getSubscriptionCreateSetup,
  isDonationCheckoutSession,
  getSubscriptionLineItems,
  SlippageOptions,
} from '../../libs/session';
import { getDiscountRecordsForCheckout } from '../../libs/discount/coupon';
import {
  expandSubscriptionItems,
  getSubscriptionPaymentAddress,
  getSubscriptionStakeSetup,
} from '../../libs/subscription';
import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';

import { invoiceQueue } from '../../queues/invoice';
import { AutoRechargeConfig, type TLineItemExpanded } from '../../store/models';
import { CheckoutSession } from '../../store/models/checkout-session';
import { Customer } from '../../store/models/customer';
import { Invoice, TInvoice } from '../../store/models/invoice';
import { InvoiceItem } from '../../store/models/invoice-item';
import { PaymentCurrency } from '../../store/models/payment-currency';
import { PaymentIntent } from '../../store/models/payment-intent';
import { PaymentMethod } from '../../store/models/payment-method';
import { Price } from '../../store/models/price';
import { PriceQuote } from '../../store/models/price-quote';
import { SetupIntent } from '../../store/models/setup-intent';
import { Subscription } from '../../store/models/subscription';
import { ensureInvoiceAndItems } from '../../libs/invoice';
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../../libs/constants';
import { returnStakeQueue } from '../../queues/subscription';

type Result = {
  checkoutSession: CheckoutSession;
  customer: Customer;
  paymentIntent?: PaymentIntent;
  subscription?: Subscription;
  subscriptions?: Subscription[];
  paymentCurrency: PaymentCurrency;
  paymentMethod: PaymentMethod;
  invoice?: Invoice;
};

export async function ensureCheckoutSession(checkoutSessionId: string) {
  const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
  if (!checkoutSession) {
    throw new Error('Checkout session not found');
  }
  if (checkoutSession.isImmutable()) {
    throw new Error('Checkout session is immutable');
  }

  return checkoutSession;
}

export async function ensurePaymentIntent(
  checkoutSessionId: string,
  userDid?: string,
  skipCustomer?: boolean
): Promise<Result> {
  const checkoutSession = await ensureCheckoutSession(checkoutSessionId);

  let paymentCurrencyId;
  let paymentMethodId;

  let paymentIntent;
  if (checkoutSession.payment_intent_id) {
    paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
    if (!paymentIntent) {
      throw new Error('Payment intent not found');
    }
    if (paymentIntent.status === 'succeeded') {
      throw new Error('Payment intent completed');
    }
    if (paymentIntent.status === 'canceled') {
      throw new Error('Payment intent canceled');
    }
    if (paymentIntent.status === 'processing') {
      throw new Error('Payment intent processing');
    }

    paymentCurrencyId = paymentIntent.currency_id;
    paymentMethodId = paymentIntent.payment_method_id;
  }

  const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);

  let subscriptions: Subscription[] = [];
  let primarySubscription: Subscription | null = null;
  if (subscriptionIds.length > 0) {
    // @ts-ignore
    subscriptions = await Promise.all(subscriptionIds.filter(Boolean).map((id) => Subscription.findByPk(id)));

    subscriptions = subscriptions.filter(Boolean);
    for (const subscription of subscriptions) {
      if (subscription && subscription.status !== 'incomplete') {
        throw new Error(`Subscription ${subscription.id} is not in incomplete status: ${subscription.status}`);
      }
    }
    if (subscriptions.length > 0) {
      // @ts-ignore
      primarySubscription = subscriptions.find((sub) => sub.metadata?.is_primary_subscription) || subscriptions[0];
      paymentCurrencyId = primarySubscription?.currency_id;
      paymentMethodId = primarySubscription?.default_payment_method_id;
    }
  }
  let customer = null;

  if (!skipCustomer) {
    // 检查是否为打赏场景
    const isDonation = isDonationCheckoutSession(checkoutSession);

    // if donation, create customer if not exists
    if (isDonation && !checkoutSession.customer_id && userDid) {
      customer = await Customer.findByPkOrDid(userDid);
      if (!customer) {
        const { user } = await blocklet.getUser(userDid);
        if (user) {
          customer = await Customer.create({
            did: userDid,
            email: user.email,
            name: user.fullName || userDid,
            description: user.remark,
            metadata: { fromDonation: true },
            livemode: checkoutSession.livemode,
            phone: user.phone,
            address: Customer.formatAddressFromUser(user),
            delinquent: false,
            balance: '0',
            next_invoice_sequence: 1,
            invoice_prefix: Customer.getInvoicePrefix(),
          });
          logger.info('Customer created for donation', { userDid, customerId: customer.id });
        } else {
          customer = await Customer.create({
            did: userDid,
            email: '',
            name: 'anonymous',
            description: 'Anonymous customer',
            metadata: { fromDonation: true, anonymous: true },
            livemode: checkoutSession.livemode,
            phone: '',
            delinquent: false,
            balance: '0',
            next_invoice_sequence: 1,
            invoice_prefix: Customer.getInvoicePrefix(),
          });
        }
      }

      if (customer) {
        await checkoutSession.update({ customer_id: customer.id, customer_did: customer.did });
        if (paymentIntent) {
          await paymentIntent.update({ customer_id: customer.id });
        }
        logger.info('Customer associated with donation', {
          userDid,
          customerId: customer.id,
          checkoutSessionId: checkoutSession.id,
          paymentIntentId: paymentIntent?.id,
        });
      }
    } else {
      // 非打赏场景或已有客户ID的情况
      customer = await Customer.findByPk(checkoutSession.customer_id);
    }
    if (!customer) {
      throw new Error('Customer not found');
    }
  }

  const [paymentMethod, paymentCurrency] = await Promise.all([
    PaymentMethod.findByPk(paymentMethodId),
    PaymentCurrency.findByPk(paymentCurrencyId),
  ]);
  if (!paymentMethod) {
    throw new Error('Payment method not found');
  }
  if (!paymentCurrency) {
    throw new Error('Payment currency not found');
  }

  if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
    throw new Error(`Payment method ${paymentMethod.type} should not be here`);
  }

  checkoutSession.line_items = await Price.expand(checkoutSession.line_items, { upsell: true });

  return {
    checkoutSession,
    paymentIntent,
    customer: customer as Customer,
    subscription: primarySubscription as Subscription,
    subscriptions,
    paymentMethod,
    paymentCurrency,
  };
}

export async function ensureSetupIntent(checkoutSessionId: string, skipInvoice?: boolean) {
  const checkoutSession = await ensureCheckoutSession(checkoutSessionId);

  if (!checkoutSession.setup_intent_id) {
    throw new Error('SetupIntent for checkoutSession not found');
  }
  if (!checkoutSession.subscription_id) {
    throw new Error('Subscription for checkoutSession not found');
  }

  const setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
  if (!setupIntent) {
    throw new Error('SetupIntent not found for checkoutSession');
  }
  if (setupIntent.isImmutable()) {
    throw new Error('SetupIntent completed or canceled for checkoutSession');
  }
  if (setupIntent.status === 'processing') {
    throw new Error('SetupIntent processing for checkoutSession');
  }

  const subscription = await Subscription.findByPk(checkoutSession.subscription_id);
  if (!subscription) {
    throw new Error('Subscription not found for checkoutSession');
  }
  if (subscription.status !== 'incomplete') {
    throw new Error('Subscription is not in incomplete status for checkoutSession');
  }

  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    throw new Error('Customer not found for checkoutSession');
  }

  const [paymentMethod, paymentCurrency] = await Promise.all([
    PaymentMethod.findByPk(subscription.default_payment_method_id),
    PaymentCurrency.findByPk(subscription.currency_id),
  ]);
  if (!paymentMethod) {
    throw new Error('Payment method not found for checkoutSession');
  }
  if (!paymentCurrency) {
    throw new Error('Payment currency not found for checkoutSession');
  }

  if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
    throw new Error(`Payment method ${paymentMethod.type} should not be here`);
  }

  checkoutSession.line_items = await Price.expand(checkoutSession.line_items, { upsell: true });

  let invoice;
  if (subscription && !skipInvoice) {
    if (checkoutSession.invoice_id) {
      invoice = await Invoice.findByPk(checkoutSession.invoice_id);
    } else {
      // Get discount information for this checkout session
      let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } =
        {
          appliedDiscounts: [],
          discountBreakdown: [],
        };

      try {
        discountInfo = await getDiscountRecordsForCheckout({
          checkoutSessionId: checkoutSession.id,
          customerId: customer.id,
        });
      } catch (error) {
        logger.warn('Failed to get discount records for setup intent checkout session', {
          checkoutSessionId: checkoutSession.id,
          customerId: customer.id,
          error: error.message,
        });
      }

      // generate empty invoice here with discount information
      const result = await ensureInvoiceAndItems({
        customer,
        currency: paymentCurrency,
        subscription,
        lineItems: [],
        trialing: !!checkoutSession.subscription_data?.trial_period_days,
        metered: false,
        props: {
          livemode: checkoutSession.livemode,
          description: checkoutSession.invoice_creation?.invoice_data?.description || 'Subscription creation',
          statement_descriptor: getStatementDescriptor(checkoutSession.line_items),
          period_start: subscription.current_period_start,
          period_end: subscription.current_period_end,

          auto_advance: true,
          billing_reason: 'subscription_create',

          currency_id: checkoutSession.currency_id,
          payment_intent_id: '',
          checkout_session_id: checkoutSession.id,

          total: '0',

          default_payment_method_id: subscription.default_payment_method_id as string,

          discounts: discountInfo.appliedDiscounts,
          total_discount_amounts: discountInfo.discountBreakdown,

          custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
          footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
          metadata: checkoutSession.invoice_creation?.invoice_data?.metadata || {},
        } as unknown as Invoice,
      });
      invoice = result.invoice;

      logger.info(`Invoice created for checkoutSession ${checkoutSession.id} in setup mode: ${invoice.id}`, {
        hasDiscounts: discountInfo.appliedDiscounts.length > 0,
        discountCount: discountInfo.appliedDiscounts.length,
      });

      // persist invoice id
      await checkoutSession.update({ invoice_id: invoice.id });
      await subscription.update({ latest_invoice_id: invoice.id });
    }
  }

  return {
    checkoutSession,
    setupIntent,
    customer,
    subscription,
    paymentMethod,
    paymentCurrency,
    invoice,
  };
}

export async function ensureSubscriptionDelegation(subscriptionId: string) {
  const subscription = (await Subscription.findOne({
    where: { id: subscriptionId },
    include: [
      { model: PaymentCurrency, as: 'paymentCurrency' },
      { model: PaymentMethod, as: 'paymentMethod' },
    ],
  })) as
    | (Subscription & { paymentCurrency: PaymentCurrency; paymentMethod: PaymentMethod; items?: TLineItemExpanded[] })
    | null;
  if (!subscription) {
    throw new Error('Subscription not found');
  }
  if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(subscription.paymentMethod?.type)) {
    throw new Error(`Payment method ${subscription.paymentMethod?.type} not supported for delegation`);
  }
  subscription.items = await expandSubscriptionItems(subscription.id);
  return {
    paymentCurrency: subscription.paymentCurrency!,
    paymentMethod: subscription.paymentMethod!,
    subscription,
    payerAddress: getSubscriptionPaymentAddress(subscription, subscription.paymentMethod?.type),
  };
}

type Args = {
  checkoutSession: CheckoutSession;
  customer: Customer;
  paymentIntent?: PaymentIntent;
  subscription?: Subscription;
  props?: Partial<TInvoice>;
  subscriptions?: Subscription[];
  lineItems?: TLineItemExpanded[];
};

export async function ensureInvoiceForCheckout({
  checkoutSession,
  customer,
  paymentIntent,
  subscription,
  props,
  subscriptions,
  lineItems,
}: Args): Promise<{ invoice: Invoice | null; items: InvoiceItem[] }> {
  const isGroupInvoice = subscriptions && subscriptions.length > 1;
  // invoices are optional when checkout session is in payment mode
  if (checkoutSession.mode === 'payment' && !checkoutSession.invoice_creation?.enabled) {
    logger.warn('Invoice creation disabled for payment mode');
    return { invoice: null, items: [] };
  }

  const existingInvoice = checkoutSession.invoice_id || (isGroupInvoice && subscription?.latest_invoice_id);
  // Do not create invoice if it's already created
  if (existingInvoice) {
    logger.info(`Invoice already created for checkout session ${checkoutSession.id}: ${existingInvoice}`);
    const invoice = await Invoice.findByPk(existingInvoice);
    if (invoice) {
      if (invoice.status === 'paid') {
        logger.info(`Invoice already paid for checkout session ${checkoutSession.id}: ${existingInvoice}`);
        throw new Error('Invoice already paid');
      }

      // invoice currency is aligned
      if (invoice.currency_id === checkoutSession.currency_id && invoice.status !== 'draft') {
        await invoice.update({ status: 'open' });
        logger.info(`Invoice status reset for checkout session ${checkoutSession.id}: ${existingInvoice}`);

        if (invoice.payment_intent_id) {
          await PaymentIntent.update({ status: 'requires_capture' }, { where: { id: invoice.payment_intent_id } });
          logger.info(
            `PaymentIntent status reset for checkout session ${checkoutSession.id}: ${invoice.payment_intent_id}`
          );
        }

        // Final Freeze: Quotes in 'used' or 'payment_failed' status can be retried directly
        // No status reset needed - quote price is immutable once created

        return {
          invoice,
          items: await InvoiceItem.findAll({ where: { invoice_id: existingInvoice } }),
        };
      }

      // invalid currency not aligned: we should generate new invoice
      await invoice.update({ status: 'void' });
      logger.info(`Invoice marked void for checkout session ${checkoutSession.id}: ${existingInvoice}`);
      const method = await PaymentMethod.findByPk(invoice.default_payment_method_id);
      if (method?.type === 'stripe' && invoice.metadata?.stripe_id) {
        const client = method.getStripeClient();
        client.invoices
          .voidInvoice(invoice.metadata.stripe_id)
          .then(() => {
            logger.info(`Invoice marked void on stripe for checkout session ${checkoutSession.id}: ${existingInvoice}`);
          })
          .catch((err) => {
            logger.error(
              `Invoice marked void on stripe failed for checkout session ${checkoutSession.id}: ${existingInvoice}`,
              err
            );
          });
      }
    }
  }

  const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);

  const metadata = {
    ...(checkoutSession.invoice_creation?.invoice_data?.metadata || {}),
  };

  if (isGroupInvoice) {
    metadata.subscription_ids = subscriptions.map((sub) => sub.id);
    metadata.is_group_invoice = true;
  }

  const trialInDays = Number(checkoutSession.subscription_data?.trial_period_days || 0);
  const trialEnd = Number(checkoutSession.subscription_data?.trial_end || 0);
  const now = dayjs().unix();
  let invoiceItems = lineItems || (await Price.expand(checkoutSession.line_items, { product: true, upsell: true }));

  // For items with quote_id, fetch full quote info and attach to line item metadata
  // This ensures invoice line items have complete quote info for display
  const itemsWithQuoteIds = invoiceItems.filter((item) => (item as any).quote_id);

  if (itemsWithQuoteIds.length > 0) {
    const quoteIds = itemsWithQuoteIds.map((item) => (item as any).quote_id).filter(Boolean);
    const quotes = await PriceQuote.findAll({
      where: { id: quoteIds },
    });

    const quotesById = new Map(quotes.map((q) => [q.id, q]));

    // Fill custom_amount and metadata.quote from quotes
    invoiceItems = invoiceItems.map((item) => {
      const quoteId = (item as any).quote_id;
      if (quoteId) {
        const quote = quotesById.get(quoteId);
        if (quote) {
          return {
            ...item,
            quote_id: quote.id, // Add quote_id directly to line item for invoice item creation
            custom_amount: item.custom_amount || quote.quoted_amount,
            metadata: {
              ...((item as any).metadata || {}),
              quote_id: quote.id, // Also in metadata for audit trail
              quote: {
                id: quote.id,
                base_currency: quote.base_currency,
                base_amount: quote.base_amount,
                quoted_amount: quote.quoted_amount,
                target_currency_id: quote.target_currency_id,
                rate_currency_symbol: quote.rate_currency_symbol,
                exchange_rate: quote.exchange_rate,
                rate_provider_name: quote.rate_provider_name,
                rate_provider_id: quote.rate_provider_id,
                rate_timestamp_ms: quote.rate_timestamp_ms,
                slippage_percent: quote.slippage_percent ?? (quote.metadata as any)?.slippage?.percent ?? 0.5,
                quantity: item.quantity,
                status: quote.status,
              },
            },
          };
        }
      }
      return item;
    });

    logger.info('Enriched line items with quote metadata for invoice creation', {
      checkoutSessionId: checkoutSession.id,
      itemCount: itemsWithQuoteIds.length,
      foundQuotes: quotesById.size,
    });
  }

  // Get discount records for this checkout session
  let discountInfo: { appliedDiscounts: string[]; discountBreakdown: Array<{ amount: string; discount: string }> } = {
    appliedDiscounts: [],
    discountBreakdown: [],
  };

  // Prepare discount configuration for getCheckoutAmount
  let discountConfig;
  try {
    discountInfo = await getDiscountRecordsForCheckout({
      checkoutSessionId: checkoutSession.id,
      customerId: customer.id,
    });

    // Apply discount if we have discount records
    if (discountInfo.appliedDiscounts.length > 0 && checkoutSession.discounts?.length) {
      const firstDiscount = checkoutSession.discounts[0];
      if (firstDiscount?.coupon && firstDiscount?.promotion_code) {
        discountConfig = {
          promotionCodeId: firstDiscount.promotion_code,
          couponId: firstDiscount.coupon,
          customerId: customer.id,
        };
      }
    }
  } catch (error) {
    logger.warn('Failed to get discount records for checkout session', {
      checkoutSessionId: checkoutSession.id,
      customerId: customer.id,
      error: error.message,
    });
  }

  const totalAmount = await getCheckoutAmount(
    invoiceItems,
    checkoutSession.currency_id,
    trialInDays > 0 || trialEnd > now,
    discountConfig
  );
  const { invoice, items } = await ensureInvoiceAndItems({
    customer,
    currency: currency as PaymentCurrency,
    subscription,
    lineItems: invoiceItems,
    trialing: !!checkoutSession.subscription_data?.trial_period_days,
    metered: false,
    props: {
      livemode: checkoutSession.livemode,
      description:
        checkoutSession.invoice_creation?.invoice_data?.description ||
        paymentIntent?.description ||
        'Subscription creation',
      statement_descriptor: paymentIntent?.statement_descriptor || getStatementDescriptor(checkoutSession.line_items),
      period_start: subscription?.current_period_start ?? 0,
      period_end: subscription?.current_period_end ?? 0,

      auto_advance: !paymentIntent,
      billing_reason: subscription ? 'subscription_create' : 'manual',

      currency_id: checkoutSession.currency_id,
      payment_intent_id: paymentIntent?.id,
      checkout_session_id: checkoutSession.id,

      total: totalAmount.total,
      subtotal: totalAmount.subtotal,

      default_payment_method_id: (subscription?.default_payment_method_id ||
        paymentIntent?.payment_method_id) as string,

      custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
      footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
      metadata,

      discounts: discountInfo.appliedDiscounts,
      total_discount_amounts: discountInfo.discountBreakdown,

      ...(props || {}),
    } as unknown as Invoice,
  });

  logger.info('Invoice created for checkoutSession', {
    checkoutSessionId: checkoutSession.id,
    invoiceId: invoice.id,
    hasDiscounts: discountInfo.appliedDiscounts.length > 0,
    discountCount: discountInfo.appliedDiscounts.length,
  });

  // only update invoice_id for single invoice
  if (!isGroupInvoice) {
    await checkoutSession.update({ invoice_id: invoice.id });
  }
  if (paymentIntent) {
    await paymentIntent.update({ invoice_id: invoice.id });
  }
  if (subscription) {
    await subscription.update({ latest_invoice_id: invoice.id });
  }

  // Update quotes with invoice_id for audit trail
  // Find quote_ids from invoice items and update the quotes
  const quoteIdsFromItems = items
    .map((item) => item.metadata?.quote_id)
    .filter((id): id is string => typeof id === 'string' && id.length > 0);

  if (quoteIdsFromItems.length > 0) {
    await PriceQuote.update(
      { invoice_id: invoice.id },
      { where: { id: quoteIdsFromItems } }
    );
    logger.info('Updated quotes with invoice_id', {
      invoiceId: invoice.id,
      quoteIds: quoteIdsFromItems,
    });
  }

  return { invoice, items };
}

export async function ensureInvoicesForSubscriptions({
  checkoutSession,
  customer,
  subscriptions,
  invoiceProps,
}: Omit<Args, 'subscription'> & { subscriptions: Subscription[]; invoiceProps?: Partial<TInvoice> }): Promise<{
  invoices: Invoice[];
}> {
  if (!subscriptions?.length) {
    logger.warn('No subscriptions provided for invoice creation');
    return { invoices: [] };
  }

  const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });

  const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
    subscriptions[0]) as Subscription;
  const invoices = await Promise.all(
    subscriptions.map(async (subscription) => {
      const subItems = await getSubscriptionLineItems(subscription, lineItems, primarySubscription);
      const { invoice } = await ensureInvoiceForCheckout({
        checkoutSession,
        customer,
        subscription,
        subscriptions,
        lineItems: subItems,
        props: invoiceProps,
      });
      return invoice;
    })
  );

  const createdInvoices = invoices.filter(Boolean);

  logger.info(`Created ${createdInvoices.length} invoices for subscriptions`, {
    checkoutSessionId: checkoutSession.id,
    invoiceIds: createdInvoices.map((inv) => inv?.id),
  });

  return { invoices: createdInvoices as Invoice[] };
}

export async function ensureInvoiceForCollect(invoiceId: string, options?: { allowCreatePI?: boolean }) {
  const invoice = await Invoice.findByPk(invoiceId);
  if (!invoice) {
    throw new Error(`Invoice ${invoiceId} not found`);
  }
  if (invoice.status === 'paid') {
    throw new Error(`Invoice ${invoiceId} already paid`);
  }
  if (invoice.status === 'void') {
    throw new Error(`Invoice ${invoiceId} already void`);
  }
  if (invoice.status === 'draft') {
    throw new Error(`Invoice ${invoiceId} is draft`);
  }

  if (!invoice.payment_intent_id) {
    // For open invoices, use invoiceQueue to create PI
    if (invoice.status === 'open') {
      await invoiceQueue.pushAndWait({
        id: invoice.id,
        job: { invoiceId: invoice.id, retryOnError: false, justCreate: true },
      });
      await invoice.reload();
    }

    // For uncollectible invoices with allowCreatePI flag, create PI directly
    if (invoice.status === 'uncollectible' && options?.allowCreatePI) {
      const customer = await Customer.findByPk(invoice.customer_id);
      const paymentIntent = await PaymentIntent.create({
        livemode: !!invoice.livemode,
        amount: invoice.amount_remaining,
        amount_received: '0',
        amount_capturable: '0',
        customer_id: invoice.customer_id,
        description: 'Manual payment for uncollectible invoice',
        currency_id: invoice.currency_id,
        payment_method_id: invoice.default_payment_method_id,
        invoice_id: invoice.id,
        status: 'requires_action',
        capture_method: 'manual',
        confirmation_method: 'manual',
        payment_method_types: [],
        receipt_email: customer?.email || invoice.customer_email,
        statement_descriptor: invoice.statement_descriptor,
      });
      await invoice.update({ payment_intent_id: paymentIntent.id });
      logger.info('Created PI for uncollectible invoice via allowCreatePI', {
        invoiceId: invoice.id,
        paymentIntentId: paymentIntent.id,
      });
    }
  }

  const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
  if (!paymentIntent) {
    throw new Error(`Payment intent not found for invoice ${invoiceId}`);
  }
  if (paymentIntent.status === 'canceled') {
    throw new Error(`Payment intent already canceled for invoice ${invoiceId}`);
  }
  if (paymentIntent.status === 'succeeded') {
    await invoice.update({ status: 'paid' });
    throw new Error(`Payment intent already succeeded for invoice ${invoiceId}`);
  }

  await paymentIntent.update({ status: 'requires_capture' });

  const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
  const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);

  return {
    invoice,
    paymentIntent,
    paymentCurrency: paymentCurrency as PaymentCurrency,
    paymentMethod: paymentMethod as PaymentMethod,
  };
}

export async function ensureSubscriptionRecharge(subscriptionId: string) {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    throw new Error(`Subscription ${subscriptionId} not found`);
  }
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    throw new Error(`Currency ${subscription.currency_id} not found`);
  }
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
  }
  // @ts-ignore
  const receiverAddress = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
  if (!receiverAddress) {
    throw new Error(`Receiver address not found for subscription ${subscriptionId}`);
  }

  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    throw new Error(`Customer ${subscription.customer_id} not found`);
  }
  return {
    paymentCurrency: paymentCurrency as PaymentCurrency,
    paymentMethod: paymentMethod as PaymentMethod,
    receiverAddress,
    subscription,
    customer,
  };
}

export async function ensureAccountRecharge(customerId: string, currencyId: string, rechargeAddress?: string) {
  const customer = await Customer.findByPkOrDid(customerId);
  if (!customer) {
    throw new Error(`Customer ${customerId} not found`);
  }
  const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
  if (!paymentCurrency) {
    throw new Error(`Currency ${currencyId} not found`);
  }
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
  }

  const receiverAddress = rechargeAddress || customer.did;

  return {
    paymentCurrency: paymentCurrency as PaymentCurrency,
    paymentMethod: paymentMethod as PaymentMethod,
    customer,
    receiverAddress,
  };
}
export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
  let chainInfo = { type: 'none', id: 'none', host: 'none' };
  if (method.type === 'arcblock') {
    chainInfo = {
      type: 'arcblock',
      id: method.settings?.arcblock?.chain_id as string,
      host: method.settings?.arcblock?.api_host as string,
    };
  }
  if (method.type === 'ethereum') {
    chainInfo = {
      type: 'ethereum',
      // @ts-ignore
      id: method.settings?.ethereum?.chain_id as number,
      host: method.settings?.ethereum?.api_host as string,
    };
  }
  if (method.type === 'base') {
    chainInfo = {
      type: 'ethereum',
      // @ts-ignore
      id: method.settings?.base?.chain_id as number,
      host: method.settings?.base?.api_host as string,
    };
  }

  return {
    description: `Select account to ${action}`,
    chainInfo,
  };
}

export async function getDelegationTxClaim({
  userDid,
  userPk,
  nonce,
  mode,
  data,
  items,
  paymentCurrency,
  paymentMethod,
  trialing = false,
  billingThreshold = 0,
  requiredStake = true,
  slippageConfig,
}: {
  userDid: string;
  userPk: string;
  nonce: string;
  mode: string;
  data: any;
  items: TLineItemExpanded[];
  paymentCurrency: PaymentCurrency;
  paymentMethod: PaymentMethod;
  trialing: boolean;
  billingThreshold?: number;
  requiredStake?: boolean;
  requiredAmount?: string;
  slippageConfig?: {
    mode?: 'percent' | 'rate';
    percent?: number;
    min_acceptable_rate?: string;
    base_currency?: string;
  };
}) {
  // Calculate authorization amount
  // When slippage_config with min_acceptable_rate is provided, use it for precise calculation
  let amount: string;
  if (slippageConfig?.min_acceptable_rate) {
    const slippageOptions: SlippageOptions = {
      percent: slippageConfig.percent ?? 0.5,
      minAcceptableRate: slippageConfig.min_acceptable_rate,
      currencyDecimal: paymentCurrency.decimal,
    };
    const setup = getSubscriptionCreateSetup(items, paymentCurrency.id, 0, 0, slippageOptions);
    amount = setup.amount.setup;
    logger.info('getDelegationTxClaim: Using slippage_config for authorization calculation', {
      mode,
      slippageConfig,
      authorizationAmount: amount,
    });
  } else {
    amount = await getFastCheckoutAmount({ items, mode, currencyId: paymentCurrency.id });
  }

  const address = toDelegateAddress(userDid, wallet.address);
  const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
  let tokenRequirements = await getTokenRequirements({
    items,
    mode,
    trialing,
    billingThreshold,
    paymentMethod,
    paymentCurrency,
    requiredStake,
  });

  if (mode === 'delegation') {
    tokenRequirements = [];
  }
  if (paymentMethod.type === 'arcblock') {
    return {
      type: 'DelegateTx',
      description: 'Sign the delegation to continue',
      wallet: fromPublicKey(userPk, toTypeInfo(userDid)),
      data: {
        itx: {
          address,
          to: wallet.address,
          ops: [{ typeUrl: OCAP_PAYMENT_TX_TYPE, limit: { tokens: tokenLimits, assets: [] } }],
          data,
        },
      },
      nonce,
      requirement: {
        tokens: tokenRequirements,
      },
      meta: {
        purpose: 'delegation',
        address,
      },
      chainInfo: {
        host: paymentMethod.settings?.arcblock?.api_host as string,
        id: paymentMethod.settings?.arcblock?.chain_id as string,
      },
    };
  }

  if (EVM_CHAIN_TYPES.includes(paymentMethod.type) && paymentCurrency.contract) {
    const requirement = tokenRequirements.find((x) => x.address === paymentCurrency.contract);
    const limit = tokenLimits.find((x) => x.address === paymentCurrency.contract);
    // logger.info('getDelegationTxClaim base', {
    //   address: ethWallet.address,
    //   amount: mode === 'setup' ? requirement!.value : limit!.totalAllowance,
    //   contract: paymentCurrency.contract,
    //   mode,
    //   requirement,
    //   limit,
    // });
    return {
      type: 'eth:transaction',
      description: 'Sign the approval to continue',
      data: toBase58(
        Buffer.from(
          JSON.stringify({
            // @ts-ignore
            network: paymentMethod.settings?.[paymentMethod.type]?.chain_id,
            tx: await encodeApproveItx(
              paymentMethod.getEvmClient(),
              ethWallet.address,
              mode === 'setup' ? requirement!.value : limit!.totalAllowance,
              paymentCurrency.contract
            ),
          }),
          'utf-8'
        )
      ),
      nonce,
      meta: {
        purpose: 'delegation',
        address,
      },
    };
  }

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

export function getStakeAmount(
  subscription: Subscription,
  paymentCurrency: PaymentCurrency,
  items: TLineItemExpanded[]
) {
  const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
  const minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);
  const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
  const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
  const amount = staking.licensed.add(staking.metered).toString();
  return amount;
}

export async function getStakeTxClaim({
  userDid,
  userPk,
  subscription,
  items,
  paymentCurrency,
  paymentMethod,
  subscriptions,
}: {
  userDid: string;
  userPk: string;
  subscription: Subscription;
  items: TLineItemExpanded[];
  paymentCurrency: PaymentCurrency;
  paymentMethod: PaymentMethod;
  subscriptions?: Subscription[];
}) {
  let billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
  let minStakeAmount = Number(subscription.billing_thresholds?.stake_gte || 0);

  const hasGrouping = subscriptions && subscriptions.length > 1;
  if (hasGrouping) {
    const primarySubscription = subscriptions[0] as Subscription;
    // use the settings of the primary subscription, not the scattered staking
    billingThreshold = Number(primarySubscription.billing_thresholds?.amount_gte || 0);
    minStakeAmount = Number(primarySubscription.billing_thresholds?.stake_gte || 0);

    logger.info('Using primary subscription for staking', {
      primarySubscriptionId: primarySubscription.id,
      billingThreshold,
      minStakeAmount,
      allSubscriptionIds: subscriptions.map((s) => s.id),
    });
  }

  const threshold = fromTokenToUnit(Math.max(billingThreshold, minStakeAmount), paymentCurrency.decimal);
  const staking = getSubscriptionStakeSetup(items, paymentCurrency.id, threshold.toString());
  const amount = staking.licensed.add(staking.metered).toString();

  logger.info('getStakeTxClaim', {
    subscriptionId: subscription.id,
    allSubscriptions: subscriptions?.map((s) => s.id) || [],
    billingThreshold,
    minStakeAmount,
    threshold: threshold.toString(),
    staking,
    amount: amount.toString(),
  });

  if (paymentMethod.type === 'arcblock') {
    // create staking data
    const client = paymentMethod.getOcapClient();

    const stakeId = hasGrouping ? `stake-group-${subscription.id}` : subscription.id;

    const address = await getCustomerStakeAddress(userDid, stakeId);
    const { state } = await client.getStakeState({ address });
    const data = {
      type: 'json',
      value: Object.assign(
        {
          appId: wallet.address,
          subscriptionId: subscription.id,
          ...(hasGrouping
            ? {
                subscriptionGroup: true,
                subscriptionIds: subscriptions.map((s) => s.id),
              }
            : {}),
        },
        JSON.parse(state?.data?.value || '{}')
      ),
    };

    // Build slippage options with min_acceptable_rate for precise authorization calculation
    const slippageConfig = subscription.slippage_config;
    const slippageOptions = {
      percent: slippageConfig?.percent ?? 0.5,
      minAcceptableRate: slippageConfig?.min_acceptable_rate,
      currencyDecimal: paymentCurrency.decimal,
    };
    const setup = getSubscriptionCreateSetup(items, paymentCurrency.id, 0, 0, slippageOptions);

    return {
      type: 'StakeTx',
      description: hasGrouping
        ? `Stake for ${subscriptions.length} subscriptions`
        : `Stake for subscription ${subscription.id}`,
      partialTx: {
        from: userDid,
        pk: userPk,
        itx: {
          address,
          receiver: wallet.address,
          slashers: [wallet.address],
          revokeWaitingPeriod: setup.cycle.duration / 1000, // wait for at least 1 billing cycle
          message: hasGrouping
            ? `Stake for ${subscriptions.length} subscriptions`
            : `Stake for subscription ${subscription.id}`,
          nonce: stakeId,
          inputs: [],
          data,
        },
        signatures: [],
      },
      requirement: {
        tokens: [{ address: paymentCurrency.contract as string, value: amount }],
      },
      nonce: `stake-${stakeId}`,
      meta: {
        purpose: 'staking',
        address,
        ...(hasGrouping
          ? {
              subscriptionGroup: true,
              primarySubscriptionId: subscription.id,
              allSubscriptionIds: subscriptions.map((s) => s.id),
            }
          : {}),
      },
      chainInfo: {
        host: paymentMethod.settings?.arcblock?.api_host as string,
        id: paymentMethod.settings?.arcblock?.chain_id as string,
      },
    };
  }

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

export async function getOverdraftProtectionStakeTxClaim({
  userDid,
  userPk,
  subscription,
  paymentCurrency,
  paymentMethod,
  stakeAmount,
  items,
}: {
  userDid: string;
  userPk: string;
  subscription: Subscription;
  paymentCurrency: PaymentCurrency;
  paymentMethod: PaymentMethod;
  stakeAmount: string;
  items: TLineItemExpanded[];
}) {
  // create staking amount
  logger.info('getStakeTxClaim', {
    subscriptionId: subscription.id,
    paymentCurrencyId: paymentCurrency.id,
    paymentMethodId: paymentMethod.id,
    stakeAmount,
  });

  if (paymentMethod.type === 'arcblock') {
    // create staking data
    const client = paymentMethod.getOcapClient();
    const nonce = `overdraft-protection-${subscription.id}`;
    const address = await getCustomerStakeAddress(userDid, nonce);
    const { state } = await client.getStakeState({ address });
    const data = {
      type: 'json',
      value: Object.assign(
        {
          appId: wallet.address,
          subscriptionId: subscription.id,
        },
        JSON.parse(state?.data?.value || '{}')
      ),
    };

    // Build slippage options with min_acceptable_rate for precise authorization calculation
    const slippageConfig = subscription.slippage_config;
    const slippageOptions = {
      percent: slippageConfig?.percent ?? 0.5,
      minAcceptableRate: slippageConfig?.min_acceptable_rate,
      currencyDecimal: paymentCurrency.decimal,
    };
    const setup = getSubscriptionCreateSetup(items, paymentCurrency.id, 0, 0, slippageOptions);

    return {
      type: 'StakeTx',
      description: `Stake to complete subscription ${subscription.id} SubGuard`,
      partialTx: {
        from: userDid,
        pk: userPk,
        itx: {
          address,
          receiver: wallet.address,
          slashers: [wallet.address],
          revokeWaitingPeriod: setup.cycle.duration / 1000, // wait for at least 1 billing cycle
          message: `Stake for subscription ${subscription.id} SubGuard`,
          nonce,
          inputs: [],
          data,
        },
        signatures: [],
      },
      requirement: {
        tokens: [{ address: paymentCurrency.contract as string, value: stakeAmount }],
      },
      nonce: `stake-${subscription.id}`,
      meta: {
        purpose: 'staking',
        address,
      },
      chainInfo: {
        host: paymentMethod.settings?.arcblock?.api_host as string,
        id: paymentMethod.settings?.arcblock?.chain_id as string,
      },
    };
  }

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

export type TokenRequirementArgs = {
  items: TLineItemExpanded[];
  mode: string;
  paymentMethod: PaymentMethod;
  paymentCurrency: PaymentCurrency;
  trialing: boolean;
  billingThreshold: number;
  requiredStake?: boolean;
};

export async function getTokenRequirements({
  items,
  mode,
  paymentMethod,
  paymentCurrency,
  trialing = false,
  billingThreshold = 0,
  requiredStake,
}: TokenRequirementArgs) {
  const tokenRequirements: { address: string; value: string }[] = [];
  let amount = await getFastCheckoutAmount({ items, mode, currencyId: paymentCurrency.id, trialing: !!trialing });

  const addStakeRequired =
    requiredStake && ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup');

  if (!addStakeRequired && amount === '0') {
    return tokenRequirements;
  }

  // If the app has not staked, we need to add the gas fee to the amount
  if ((await hasStakedForGas(paymentMethod)) === false) {
    const maxGas = await estimateMaxGasForTx(paymentMethod);
    // pay with the base currency? just add gas to it
    if (paymentCurrency.is_base_currency) {
      amount = new BN(amount).add(new BN(maxGas)).toString();
      tokenRequirements.push({ address: paymentCurrency.contract as string, value: amount });
    } else {
      tokenRequirements.push({ address: paymentCurrency.contract as string, value: amount });
      const baseCurrency = await PaymentCurrency.findOne({
        where: { active: true, is_base_currency: true, payment_method_id: paymentMethod.id },
      });
      if (baseCurrency) {
        tokenRequirements.push({ address: baseCurrency.contract as string, value: maxGas });
      } else {
        throw new Error('Base currency not found since app has not staked for gas');
      }
    }
  } else {
    tokenRequirements.push({ address: paymentCurrency.contract as string, value: amount });
  }

  // Add stake requirement to token requirement
  if (addStakeRequired) {
    const staking = getSubscriptionStakeSetup(
      items,
      paymentCurrency.id,
      fromTokenToUnit(billingThreshold, paymentCurrency.decimal).toString()
    );
    const exist = tokenRequirements.find((x) => x.address === paymentCurrency.contract);
    if (exist) {
      exist.value = new BN(exist.value).add(staking.licensed).add(staking.metered).toString();
    } else {
      tokenRequirements.push({
        address: paymentCurrency.contract as string,
        value: staking.licensed.add(staking.metered).toString(),
      });
    }
  }

  return tokenRequirements;
}

export async function ensureSubscription(subscriptionId: string): Promise<Result & { customer: Customer }> {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    throw new Error(`Subscription not found: ${subscriptionId}`);
  }

  if (subscription.isActive() === false) {
    throw new Error(`Subscription ${subscriptionId} is not active`);
  }

  const paymentCurrencyId = subscription.currency_id;
  const paymentMethodId = subscription.default_payment_method_id;

  const [paymentMethod, paymentCurrency, invoice] = await Promise.all([
    PaymentMethod.findByPk(paymentMethodId),
    PaymentCurrency.findByPk(paymentCurrencyId),
    Invoice.findByPk(subscription.latest_invoice_id),
  ]);
  if (!paymentMethod) {
    throw new Error(`Payment method not found for subscription ${subscriptionId}`);
  }
  if (!paymentCurrency) {
    throw new Error(`Payment currency not found for subscription ${subscriptionId}`);
  }

  if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
    throw new Error(`Payment method ${paymentMethod.type} should not be here`);
  }

  // @ts-ignore
  subscription.items = await expandSubscriptionItems(subscription.id);

  return {
    // @ts-ignore
    checkoutSession: null,
    subscription,
    paymentMethod,
    paymentCurrency,
    // @ts-ignore
    invoice,
    // @ts-ignore
    customer: await Customer.findByPk(subscription.customer_id),
  };
}

export async function ensureChangePaymentContext(subscriptionId: string) {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    throw new Error(`Subscription not found: ${subscriptionId}`);
  }

  const context = subscription.metadata?.changePayment || {};
  if (!context || !context.setup_intent_id) {
    throw new Error(`Change payment context not found: ${subscriptionId}`);
  }

  const setupIntent = await SetupIntent.findByPk(context.setup_intent_id);
  if (!setupIntent) {
    throw new Error(`SetupIntent not found for subscription payment change ${subscriptionId}`);
  }
  if (setupIntent.status === 'succeeded') {
    throw new Error(`SetupIntent completed for subscription payment change ${subscriptionId}`);
  }

  const paymentCurrencyId = setupIntent.currency_id;
  const paymentMethodId = setupIntent.payment_method_id;

  const [paymentMethod, paymentCurrency] = await Promise.all([
    PaymentMethod.findByPk(paymentMethodId),
    PaymentCurrency.findByPk(paymentCurrencyId),
  ]);
  if (!paymentMethod) {
    throw new Error(`Payment method not found for SetupIntent ${setupIntent.id}`);
  }
  if (!paymentCurrency) {
    throw new Error(`Payment currency not found for SetupIntent ${setupIntent.id}`);
  }

  if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
    throw new Error(`Payment method ${paymentMethod.type} should not be here`);
  }

  // @ts-ignore
  subscription.items = await expandSubscriptionItems(subscription.id);

  return {
    subscription,
    setupIntent,
    paymentMethod,
    paymentCurrency,
    customer: await Customer.findByPk(subscription.customer_id),
  };
}

export async function ensurePayerChangeContext(subscriptionId: string) {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    throw new Error(`Subscription not found: ${subscriptionId}`);
  }
  if (!['active', 'trialing', 'past_due'].includes(subscription.status)) {
    throw new Error(`Subscription ${subscriptionId} is not in a valid status to change payer`);
  }
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
  if (!paymentMethod) {
    throw new Error(`Payment method not found for subscription ${subscriptionId}`);
  }
  const payerAddress = getSubscriptionPaymentAddress(subscription, paymentMethod?.type);
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    throw new Error(`PaymentCurrency ${subscription.currency_id} not found for subscription ${subscriptionId}`);
  }

  // @ts-ignore
  subscription.items = await expandSubscriptionItems(subscription.id);

  return {
    subscription,
    paymentCurrency,
    paymentMethod,
    customer: await Customer.findByPk(subscription.customer_id),
    payerAddress,
  };
}

export async function ensureReStakeContext(subscriptionId: string) {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    throw new Error(`Subscription not found: ${subscriptionId}`);
  }
  if (subscription.status === 'canceled') {
    throw new Error(`Subscription ${subscriptionId} not recoverable from cancellation`);
  }
  if (!subscription.cancel_at_period_end) {
    throw new Error(`Subscription ${subscriptionId} not recoverable from cancellation config`);
  }
  if (subscription.cancelation_details?.reason === 'payment_failed') {
    throw new Error(`Subscription ${subscriptionId} not recoverable from payment failed`);
  }

  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    throw new Error(`PaymentCurrency ${subscription.currency_id} not found for subscription ${subscriptionId}`);
  }
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    throw new Error(`Payment method not found for subscription ${subscriptionId}`);
  }

  if (paymentMethod.type !== 'arcblock') {
    throw new Error(`Payment method ${paymentMethod.type} not supported for subscription ${subscriptionId}`);
  }

  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    throw new Error(`Customer not found for subscription ${subscriptionId}`);
  }

  // @ts-ignore
  subscription.items = await expandSubscriptionItems(subscription.id);

  return {
    subscription,
    paymentCurrency,
    paymentMethod,
    customer,
    payerAddress: getSubscriptionPaymentAddress(subscription, paymentMethod?.type),
  };
}
export async function ensureSubscriptionForCollectBatch(
  subscriptionId?: string,
  currencyId?: string,
  customerId?: string
) {
  if (!currencyId) {
    throw new Error('Currency ID must be provided');
  }
  if (!subscriptionId && !customerId) {
    throw new Error('Either subscriptionId or customerId must be provided');
  }

  const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
  if (!paymentCurrency) {
    throw new Error(`PaymentCurrency ${currencyId} not found when prepare batch collect`);
  }

  let subscription;
  let searchCustomerId = customerId;

  if (subscriptionId) {
    subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      throw new Error(`Subscription ${subscriptionId} not found when prepare batch collect`);
    }
    searchCustomerId = subscription.customer_id;
  } else if (customerId) {
    const customer = await Customer.findByPkOrDid(customerId);
    if (!customer) {
      throw new Error(`Customer ${customerId} not found when prepare batch collect`);
    }
    searchCustomerId = customer.id;
  }

  const [summary, detail] = await Invoice.getUncollectibleAmount({
    ...(subscriptionId ? { subscriptionId } : {}),
    customerId: searchCustomerId,
    currencyId,
  });
  if (isEmpty(summary) || !summary[currencyId] || summary[currencyId] === '0') {
    throw new Error('No uncollectible invoice found to batch collect');
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    throw new Error(`Payment method not found for currency ${currencyId}`);
  }

  return {
    subscription,
    paymentCurrency: paymentCurrency as PaymentCurrency,
    paymentMethod: paymentMethod as PaymentMethod,
    amount: summary[currencyId],
    invoices: detail[currencyId],
  };
}

export async function ensureSubscriptionForOverdraftProtection(subscriptionId: string, amount: string) {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    throw new Error(`Subscription ${subscriptionId} not found when prepare SubGuard`);
  }
  // @ts-ignore
  subscription.items = await expandSubscriptionItems(subscription.id);
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    throw new Error(`PaymentCurrency ${subscription.currency_id} not found when prepare SubGuard`);
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);

  if (!paymentMethod) {
    throw new Error(`Payment method not found for subscription ${subscriptionId}`);
  }
  if (paymentMethod.type !== 'arcblock') {
    throw new Error(`Payment method ${paymentMethod.type} not supported for SubGuard`);
  }

  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    throw new Error(`Customer not found for subscription ${subscriptionId}`);
  }

  return {
    subscription,
    paymentCurrency: paymentCurrency as PaymentCurrency,
    paymentMethod: paymentMethod as PaymentMethod,
    customer,
    stakeAmount: fromTokenToUnit(amount, paymentCurrency.decimal).toString(),
  };
}

async function executeSingleTransaction(
  client: any,
  claim: any,
  type: 'Delegate' | 'Stake',
  userDid: string,
  userPk: string,
  gasPayerHeaders: Record<string, string>
): Promise<string> {
  if (!claim) return '';

  const tx: Partial<Transaction> = client.decodeTx(claim.finalTx || claim.origin);
  if (claim.sig) {
    tx.signature = claim.sig;
  }

  const { buffer } = await client[`encode${type}Tx`]({ tx });
  return client[`send${type}Tx`](
    { tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
    await getGasPayerExtra(buffer, gasPayerHeaders)
  );
}

export async function executeOcapTransactions(
  userDid: string,
  userPk: string,
  claims: any[],
  paymentMethod: PaymentMethod,
  requestSource: Request | Request[] | any[],
  subscriptionId?: string,
  paymentCurrencyContract?: string,
  nonce?: string
) {
  const client = paymentMethod.getOcapClient();
  logger.info('start executeOcapTransactions', { userDid, claims });

  const delegation = claims.find((x) => x.type === 'signature' && x.meta?.purpose === 'delegation');
  const staking = claims.find((x) => x.type === 'prepareTx' && x.meta?.purpose === 'staking');

  const stakingAmount =
    staking?.requirement?.tokens?.find((x: any) => x?.address === paymentCurrencyContract)?.value || '0';

  try {
    const getHeaders = (index: number): Record<string, string> => {
      if (Array.isArray(requestSource)) {
        if (requestSource.length === 0) {
          return {};
        }
        const headerIndex = index < requestSource.length ? index : 0;
        const req = requestSource[headerIndex];
        return req ? client.pickGasPayerHeaders(req) : {};
      }

      if (requestSource && typeof requestSource === 'object') {
        return client.pickGasPayerHeaders(requestSource);
      }

      return {};
    };

    const transactions = [
      { claim: delegation, type: 'Delegate' },
      { claim: staking, type: 'Stake' },
    ];

    const txHashes = [];
    for (let i = 0; i < transactions.length; i++) {
      const { claim, type } = transactions[i]!;
      // eslint-disable-next-line no-await-in-loop
      const hash = await executeSingleTransaction(
        client,
        claim,
        type as 'Delegate' | 'Stake',
        userDid,
        userPk,
        getHeaders(i)
      );
      txHashes.push(hash);
    }

    const [delegationTxHash, stakingTxHash] = txHashes;

    return {
      tx_hash: delegationTxHash,
      payer: userDid,
      type: 'delegate',
      delegation_address: delegation?.meta?.address, // Store delegation address for migration compatibility
      staking: {
        tx_hash: stakingTxHash,
        address: await getCustomerStakeAddress(userDid, nonce || subscriptionId || ''),
      },
      stakingAmount,
    };
  } catch (err) {
    logger.error('executeOcapTransactions failed', { error: err, userDid });
    throw err;
  }
}

export async function updateStripeSubscriptionAfterChangePayment(setupIntent: SetupIntent, subscription: Subscription) {
  const { from_method: fromMethodId, to_method: toMethodId } = setupIntent.metadata || {};
  const fromMethod = await PaymentMethod.findByPk(fromMethodId);
  if (fromMethod?.type === 'stripe') {
    // pause Stripe
    const client = fromMethod?.getStripeClient();
    const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
    if (client && stripeSubscriptionId) {
      const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
      if (stripeSubscription) {
        const result = await client.subscriptions.update(stripeSubscriptionId, {
          pause_collection: {
            behavior: 'void',
          },
        });
        logger.info('stripe subscription paused on payment change', {
          subscription: subscription.id,
          stripeSubscription: stripeSubscriptionId,
          result,
        });
      }
    }
  } else {
    const toMethod = await PaymentMethod.findByPk(toMethodId);
    if (toMethod?.type === 'stripe') {
      // resume stripe
      const client = toMethod?.getStripeClient();
      const stripeSubscriptionId = subscription.payment_details?.stripe?.subscription_id as string;
      if (client && stripeSubscriptionId) {
        const stripeSubscription = await client.subscriptions.retrieve(stripeSubscriptionId);
        if (stripeSubscription.status === 'paused') {
          await client.subscriptions.resume(stripeSubscriptionId, {
            proration_behavior: 'none',
          });
          logger.info('stripe subscription resumed on payment change', {
            subscription: subscription.id,
            stripeSubscription: stripeSubscriptionId,
          });
        }
      }
    }
  }
}

export async function ensureAutoRechargeAuthorization(
  autoRechargeConfigId: string
): Promise<{ autoRechargeConfig: AutoRechargeConfig; paymentMethod: PaymentMethod; paymentCurrency: PaymentCurrency }> {
  const autoRechargeConfig = await AutoRechargeConfig.findByPk(autoRechargeConfigId);
  if (!autoRechargeConfig) {
    throw new Error(`Auto recharge config ${autoRechargeConfigId} not found`);
  }

  const price = await Price.findByPk(autoRechargeConfig.price_id);
  if (!price) {
    throw new Error(`Price ${autoRechargeConfig.price_id} not found`);
  }
  const paymentCurrency = await PaymentCurrency.findByPk(autoRechargeConfig.recharge_currency_id);
  if (!paymentCurrency) {
    throw new Error(`Payment currency ${autoRechargeConfig.recharge_currency_id} not found`);
  }

  if (paymentCurrency.payment_method_id !== autoRechargeConfig.payment_method_id) {
    await autoRechargeConfig.update({
      payment_method_id: paymentCurrency.payment_method_id,
    });
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
  }

  return {
    autoRechargeConfig,
    paymentMethod,
    paymentCurrency,
  };
}

export async function returnStakeForCanceledSubscription(subscriptionId: string) {
  if (!subscriptionId) {
    return;
  }
  try {
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      throw new Error(`Subscription ${subscriptionId} not found`);
    }
    if (subscription.status !== 'canceled') {
      throw new Error(`Subscription ${subscriptionId} is not canceled`);
    }

    if (!subscription.payment_details?.arcblock?.staking?.tx_hash) {
      throw new Error(`No staking transaction found in subscription ${subscriptionId}`);
    }
    returnStakeQueue.push({ id: `return-stake-${subscription.id}`, job: { subscriptionId: subscription.id } });
    logger.info('Subscription return stake job scheduled', {
      jobId: `return-stake-${subscription.id}`,
      subscription: subscription.id,
    });
  } catch (err) {
    logger.error('returnStakeForCanceledSubscription failed', { error: err, subscriptionId });
  }
}
