import isEmpty from 'lodash/isEmpty';

import { BN } from '@ocap/util';
import pAll from 'p-all';
import { ensureStakedForGas } from '../integrations/arcblock/stake';
import { transferErc20FromUser } from '../integrations/ethereum/token';
import { createEvent } from '../libs/audit';
import { blocklet, wallet } from '../libs/auth';
import dayjs from '../libs/dayjs';
import CustomError from '../libs/error';
import { events } from '../libs/event';
import logger from '../libs/logger';
import { getGasPayerExtra, isDelegationSufficientForPayment, checkDepositVaultAmount } from '../libs/payment';
import { validateQuoteForPayment, handleExpiredQuotePayment } from '../libs/quote-validation';
import { getQuoteService } from '../libs/quote-service';
import {
  checkRemainingStake,
  getDaysUntilCancel,
  getDaysUntilDue,
  getDueUnit,
  getMaxRetryCount,
  getMinRetryMail,
  getSubscriptionStakeAddress,
  isSubscriptionOverdraftProtectionEnabled,
  shouldCancelSubscription,
} from '../libs/subscription';
import { MAX_RETRY_COUNT, MIN_RETRY_MAIL, getNextRetry } from '../libs/util';
import { CheckoutSession } from '../store/models/checkout-session';
import { Customer } from '../store/models/customer';
import { Invoice } from '../store/models/invoice';

import { PaymentCurrency } from '../store/models/payment-currency';
import { PaymentIntent } from '../store/models/payment-intent';
import { PaymentMethod } from '../store/models/payment-method';
import { Payout } from '../store/models/payout';
import { Price } from '../store/models/price';

import { Subscription } from '../store/models/subscription';
import { SubscriptionItem } from '../store/models/subscription-item';
import type { EVMChainType, PaymentError, PaymentSettings } from '../store/models/types';
import { notificationQueue } from './notification';
import { ensureOverdraftProtectionInvoiceAndItems } from '../libs/invoice';
import { AutoRechargeConfig, Lock, MeterEvent } from '../store/models';
import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
import createQueue from '../libs/queue';
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../libs/constants';
import { getCheckoutSessionSubscriptionIds, getSubscriptionCreateSetup, SlippageOptions } from '../libs/session';
import { syncStripeSubscriptionAfterRecovery } from '../integrations/stripe/handlers/subscription';
import { getLock } from '../libs/lock';

type PaymentJob = {
  paymentIntentId: string;
  paymentSettings?: PaymentSettings;
  retryOnError?: boolean;
  ignoreMaxRetryCheck?: boolean;
  immediateRetry?: boolean;
};

type DepositVaultJob = {
  currencyId: string;
};

async function updateQuantitySold(checkoutSession: CheckoutSession) {
  const updatePromises = checkoutSession.line_items.map((item) => {
    const priceId = item.upsell_price_id || item.price_id;
    return Price.increment({ quantity_sold: Number(item.quantity) }, { where: { id: priceId } }).catch((err) => {
      logger.error('Update quantity_sold failed', {
        error: err,
        priceId,
        checkoutSessionId: checkoutSession.id,
      });
    });
  });

  await Promise.all(updatePromises);
}

const handleDepositVault = async (paymentCurrencyId: string) => {
  const { depositAmount, message, vaultAddress, paymentCurrency } = await checkDepositVaultAmount(paymentCurrencyId);
  if (depositAmount === '0') {
    logger.info(`Deposit vault skipped: ${message}`, { currencyId: paymentCurrencyId });
    return;
  }
  const payout = await Payout.create({
    livemode: paymentCurrency!.livemode,
    automatic: true,
    description: 'Deposit vault',
    amount: depositAmount,
    destination: vaultAddress!,
    payment_method_id: paymentCurrency!.payment_method_id,
    currency_id: paymentCurrency!.id,
    customer_id: '',
    payment_intent_id: '',
    status: 'pending',
    attempt_count: 0,
    attempted: false,
    next_attempt: 0,
    last_attempt_error: null,
    metadata: {
      system: true,
    },
  });
  logger.info('Deposit vault payout created', { payoutId: payout.id });
};

export const depositVaultQueue = createQueue<DepositVaultJob>({
  name: 'deposit-vault',
  onJob: async (job) => {
    await handleDepositVault(job.currencyId);
  },
  options: {
    concurrency: 1,
    maxRetries: 3,
    enableScheduledJob: true,
  },
});

export async function updateSubscriptionOnPaymentSuccess(
  paymentIntent: PaymentIntent | null,
  subscription: Subscription,
  invoice?: Invoice | null,
  triggerRenew: boolean = true
) {
  // Handle incomplete subscription activation
  if (subscription.status === 'incomplete' && invoice && invoice.id === subscription.latest_invoice_id) {
    const started = await subscription.start();
    if (started) {
      logger.info(`Subscription ${subscription.id} activated on payment done ${invoice.id}`);
    }
    return;
  }

  // Handle past_due subscription recovery (payment_failed, insufficient_credit, slippage_below_threshold, etc.)
  if (subscription.status === 'past_due') {
    await handlePastDueSubscriptionRecovery(subscription, paymentIntent);
    return;
  }

  // Handle trialing subscription
  if (subscription.status === 'trialing') {
    const started = await subscription.start();
    if (started) {
      logger.info(`Subscription ${subscription.id} trialing ended on payment done ${invoice?.id || 'credit'}`);
    }
    return;
  }

  // Trigger renewal and upgrade events
  if (triggerRenew && (!invoice || invoice.billing_reason !== 'subscription_update')) {
    if (!invoice || invoice.billing_reason === 'subscription_cycle' || paymentIntent?.capture_method === 'manual') {
      createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(console.error);
    }
  }
  if (invoice?.billing_reason === 'subscription_update') {
    createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
  }
}

export async function handlePastDueSubscriptionRecovery(
  subscription: Subscription,
  paymentIntent: PaymentIntent | null
) {
  // For credit mode with insufficient_credit reason, check meter events instead of uncollectible invoices
  if (subscription.cancelation_details?.reason === 'insufficient_credit') {
    // Check if all meter events for this subscription are completed with zero pending amount
    const [summary, detail] = await MeterEvent.getPendingAmounts({
      subscriptionId: subscription.id,
      livemode: subscription.livemode,
      currencyId: subscription.currency_id,
    });

    if (summary[subscription.currency_id] && summary[subscription.currency_id] !== '0') {
      logger.info('Subscription recovery skipped: pending meter events exist', {
        subscription: subscription.id,
        pendingEventsCount: detail[subscription.currency_id]?.length,
        pendingEventIds: detail[subscription.currency_id],
      });
      return;
    }
  } else {
    // For payment_failed reason, check uncollectible invoices
    const [result] = await Invoice.getUncollectibleAmount({ subscriptionId: subscription.id });
    if (!isEmpty(result)) {
      return;
    }
  }

  const now = dayjs().unix();

  // Reset billing cycle anchor and cancel_* if we are recovering from payment failed or insufficient credit
  if (subscription.cancel_at && subscription.cancel_at !== subscription.current_period_end) {
    if (now <= subscription.current_period_end) {
      // If payment succeeds before current_period_end, we should activate this subscription
      await subscription.update({
        status: 'active',
        cancel_at: 0,
        cancel_at_period_end: false,
        // @ts-ignore
        cancelation_details: null,
      });
      const recoveryReason =
        subscription.cancelation_details?.reason === 'insufficient_credit' ? 'credit replenished' : 'payment done';
      logger.info(
        `Subscription ${subscription.id} recovered on ${recoveryReason} ${paymentIntent?.id || 'credit'}: cancel rest`
      );
      if (paymentIntent) {
        await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
      }
      return;
    }

    // Reset billing cycle
    const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
    const lineItems = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
    // Use the subscription's slippage config if available, otherwise use default 0.5%
    const slippageConfig = subscription.slippage_config;
    const currency = await PaymentCurrency.findByPk(subscription.currency_id);
    const slippageOptions: SlippageOptions = {
      percent: slippageConfig?.percent ?? 0.5,
      minAcceptableRate: slippageConfig?.min_acceptable_rate,
      currencyDecimal: currency?.decimal,
    };
    const setup = getSubscriptionCreateSetup(lineItems, subscription.currency_id, 0, 0, slippageOptions);
    await subscription.update({
      status: 'active',
      pending_invoice_item_interval: setup.recurring,
      current_period_start: setup.period.start,
      current_period_end: setup.period.end,
      billing_cycle_anchor: setup.cycle.anchor,
      cancel_at: 0,
      cancel_at_period_end: false,
      // @ts-ignore
      cancelation_details: null,
    });

    createEvent('Subscription', 'customer.subscription.recovered', subscription).catch(console.error);
    const recoveryReason =
      subscription.cancelation_details?.reason === 'insufficient_credit' ? 'credit replenished' : 'payment done';
    logger.info(
      `Subscription ${subscription.id} recovered on ${recoveryReason} ${paymentIntent?.id || 'credit'}: cancel and billing cycle reset`
    );
    if (paymentIntent) {
      await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
    }
  } else if (subscription.cancel_at_period_end) {
    // Reset cancel_at_period_end if we are recovering from payment failed or insufficient credit
    // @ts-ignore
    await subscription.update({ status: 'active', cancel_at_period_end: false, cancelation_details: null });
    const recoveryReason =
      subscription.cancelation_details?.reason === 'insufficient_credit' ? 'credit replenished' : 'payment done';
    logger.info(
      `Subscription ${subscription.id} recovered on ${recoveryReason} ${paymentIntent?.id || 'credit'}: cancel reset`
    );
    if (paymentIntent) {
      await syncStripeSubscriptionAfterRecovery(subscription, paymentIntent.id);
    }
  } else {
    await subscription.update({ status: 'active' });
    const recoveryReason =
      subscription.cancelation_details?.reason === 'insufficient_credit' ? 'credit replenished' : 'payment done';
    logger.info(`Subscription ${subscription.id} recovered on ${recoveryReason} ${paymentIntent?.id || 'credit'}`);
  }
}

async function updateCheckoutSessionOnPaymentSuccess(
  paymentIntent: PaymentIntent,
  checkoutSession: CheckoutSession,
  invoice?: Invoice
) {
  if (checkoutSession.status !== 'open') {
    return;
  }

  // Update quantity sold for line items
  updateQuantitySold(checkoutSession).catch((err) => {
    logger.error('Updating quantity_sold for line items failed', {
      error: err,
      checkoutSessionId: checkoutSession.id,
    });
  });

  // Handle subscription mode checkout sessions
  if (['subscription', 'setup'].includes(checkoutSession.mode) && invoice?.subscription_id) {
    await checkoutSession.increment('success_subscription_count', { by: 1 });
    await checkoutSession.reload();
    const subscriptionIds = getCheckoutSessionSubscriptionIds(checkoutSession);

    if (
      checkoutSession.success_subscription_count &&
      checkoutSession.success_subscription_count >= subscriptionIds.length
    ) {
      await checkoutSession.update({
        status: 'complete',
        payment_status: 'paid',
        payment_details: paymentIntent?.payment_details || {},
      });
      logger.info('checkout session become complete on payment done', {
        id: checkoutSession.id,
      });
    }
  } else {
    // Handle non-subscription mode checkout sessions
    await checkoutSession.update({
      status: 'complete',
      payment_status: 'paid',
      payment_details: paymentIntent?.payment_details || {},
    });
    logger.info('checkout session become complete on payment done', {
      id: checkoutSession.id,
    });
  }

  logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent.id}`);
}

export const handlePaymentSucceed = async (
  paymentIntent: PaymentIntent,
  triggerRenew: boolean = true,
  slashStake: boolean = false
) => {
  // FIXME: @wangshijun we should check stripe payment here before

  if (paymentIntent.beneficiaries?.length && !slashStake) {
    Promise.all(
      paymentIntent.beneficiaries.map(async (x) => {
        let customer = await Customer.findByPkOrDid(x.address);
        if (!customer) {
          const { user } = await blocklet.getUser(x.address);
          if (user) {
            customer = await Customer.create({
              livemode: paymentIntent.livemode,
              did: user.did,
              name: user.fullName,
              email: user.email,
              phone: user.phone,
              address: Customer.formatAddressFromUser(user),
              description: user.remark,
              metadata: {},
              balance: '0',
              next_invoice_sequence: 1,
              delinquent: false,
              invoice_prefix: Customer.getInvoicePrefix(),
            });
            logger.info('Customer created on payout record', {
              paymentIntent: paymentIntent.id,
              address: x.address,
              customer: customer.id,
            });
          }
        }

        const payout = new Payout({
          livemode: paymentIntent.livemode,
          automatic: true,
          description: paymentIntent.description,
          amount: x.share,
          destination: x.address,
          payment_details: paymentIntent.payment_details,
          payment_intent_id: paymentIntent.id,
          customer_id: customer?.id || '',
          currency_id: paymentIntent.currency_id,
          payment_method_id: paymentIntent.payment_method_id,
          status: 'paid',
          attempt_count: 0,
          attempted: false,
          next_attempt: 0,
          last_attempt_error: null,
          metadata: {},
        });
        return payout.save();
      })
    )
      .then(() => logger.info('Payout records created from payment done', { paymentIntent: paymentIntent.id }))
      .catch((err) =>
        logger.error('Payout records creation failed from payment done', {
          paymentIntent: paymentIntent.id,
          error: err,
        })
      );
  }

  // Trigger vendor commission queue by invoice.paid

  let invoice;
  if (paymentIntent.invoice_id) {
    invoice = await Invoice.findByPk(paymentIntent.invoice_id);
  }

  // Handle checkout session when no invoice exists
  if (!invoice && !slashStake) {
    const checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: paymentIntent.id } });
    if (checkoutSession) {
      await updateCheckoutSessionOnPaymentSuccess(paymentIntent, checkoutSession);
    }
    return;
  }

  // Update invoice status
  if (invoice && invoice?.status !== 'paid') {
    await invoice.update({
      paid: true,
      status: 'paid',
      amount_paid: paymentIntent.amount,
      amount_remaining: '0',
      attempt_count: invoice.attempt_count + 1,
      attempted: true,
      status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
    });
    logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
  }

  // Mark quotes as paid after successful payment
  // Query quotes by invoiceId - more reliable than depending on metadata
  if (invoice?.id) {
    const quoteService = getQuoteService();
    const markedIds = await quoteService.markQuotesAsPaidByInvoice(invoice.id);
    if (markedIds.length > 0) {
      logger.info('Marked quotes as paid after successful payment', {
        invoiceId: invoice.id,
        quoteIds: markedIds,
      });
    }
  }
  // Update subscription status
  if (invoice && invoice.subscription_id && !slashStake) {
    const subscription = await Subscription.findByPk(invoice.subscription_id);
    if (subscription) {
      await updateSubscriptionOnPaymentSuccess(paymentIntent, subscription, invoice, triggerRenew);
    }
  }

  // Update checkout session
  if (invoice && invoice.checkout_session_id && !slashStake) {
    const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
    if (checkoutSession) {
      await updateCheckoutSessionOnPaymentSuccess(paymentIntent, checkoutSession, invoice);
    }
  }
};

export const doOverdraftProtection = async (
  paymentIntent: PaymentIntent,
  subscription: Subscription,
  customer: Customer,
  relatedInvoice?: Invoice
) => {
  const { enabled } = await isSubscriptionOverdraftProtectionEnabled(subscription);
  if (!enabled) {
    logger.warn('overdraft protection is not enabled', {
      subscription: subscription.id,
    });
    return;
  }
  const existProtectedInvoice = await Invoice.findOne({
    where: {
      subscription_id: subscription.id,
      billing_reason: 'overdraft_protection',
      'metadata.payment_intent_id': paymentIntent.id,
    },
  });
  if (existProtectedInvoice) {
    logger.info('overdraft protection invoice already exists', {
      invoice: existProtectedInvoice.id,
    });
    return;
  }
  try {
    const { invoice, items } = await ensureOverdraftProtectionInvoiceAndItems({
      customer,
      subscription,
      paymentIntent,
      props: {
        period_start: subscription.current_period_start,
        period_end: subscription.current_period_end,
        metadata: {
          payment_intent_id: paymentIntent.id,
          invoice_id: relatedInvoice?.id || '',
        },
      },
    });
    logger.info('ensure overdraft protection invoice and items done', {
      invoice: invoice.id,
      items: items.map((x) => x.id),
    });
  } catch (error) {
    logger.error('ensure overdraft protection invoice and items failed', {
      error,
      subscription: subscription.id,
    });
    throw error;
  }
};

type Updates = {
  retry: {
    payment: Partial<PaymentIntent>;
    invoice: Partial<Invoice>;
    minRetryMail?: number;
  };
  terminate: {
    payment: Partial<PaymentIntent>;
    invoice: Partial<Invoice>;
    minRetryMail?: number;
  };
};

export const handlePaymentFailed = async (
  paymentIntent: PaymentIntent,
  invoice: Invoice,
  error: PaymentError,
  checkSubscriptionStatus = true
) => {
  const now = dayjs().unix();
  const attemptCount = invoice.attempt_count + 1;

  const updates: Updates = {
    retry: {
      payment: { status: 'requires_capture', last_payment_error: error },
      invoice: {
        attempt_count: attemptCount,
        attempted: true,
        next_payment_attempt: getNextRetry(attemptCount),
      },
    },
    terminate: {
      payment: { status: 'requires_action', last_payment_error: error },
      invoice: {
        status: 'uncollectible',
        attempt_count: attemptCount,
        attempted: true,
        status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: now },
      },
    },
  };

  if (!invoice.subscription_id) {
    return updates.retry;
  }
  const subscription = await Subscription.findByPk(invoice.subscription_id);
  if (!subscription) {
    return updates.retry;
  }

  const { interval } = subscription.pending_invoice_item_interval;
  updates.retry.minRetryMail = getMinRetryMail(interval);
  updates.terminate.minRetryMail = getMinRetryMail(interval);

  if (checkSubscriptionStatus) {
    if (subscription.isActive() === false) {
      return updates.retry;
    }
    if (subscription.isImmutable()) {
      return updates.terminate;
    }
  }

  // check current period
  if (shouldCancelSubscription(subscription)) {
    await subscription.update({
      status: 'canceled',
      canceled_at: now,
      cancelation_details: {
        comment: 'exceed_current_period',
        feedback: 'other',
        reason: 'payment_failed',
      },
    });
    logger.warn('Subscription moved to canceled after retry exceeds current_period_end', {
      subscription: subscription.id,
      payment: paymentIntent.id,
      current_period_end: subscription.current_period_end,
      now,
    });
    return updates.terminate;
  }

  if (invoice.billing_reason === 'overdraft_protection') {
    // overdraft protection invoice is always terminated
    return updates.terminate;
  }
  // check overdraft protection, if protected, no need to check due
  const customer = await Customer.findByPk(invoice.customer_id);

  const { enabled: enableOverdraftProtection, unused: unusedAmount } =
    await isSubscriptionOverdraftProtectionEnabled(subscription);
  const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
  const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === paymentIntent?.currency_id);

  if (subscription.overdraft_protection?.enabled && !enableOverdraftProtection) {
    // overdraft protection is enabled but not enough
    const lockKey = `${subscription.id}-${paymentIntent.currency_id}-overdraft-protection-exhausted`;
    const isLock = await Lock.isLocked(lockKey);
    if (!isLock) {
      await Lock.acquire(lockKey, subscription.current_period_end);
      createEvent('Subscription', 'subscription.overdraft_protection.exhausted', subscription).catch(console.error);
    }
  }

  if (
    enableOverdraftProtection &&
    unusedAmount !== '0' &&
    new BN(unusedAmount).gte(new BN(invoicePrice?.unit_amount || '0'))
  ) {
    logger.info('do overdraft protection', {
      subscription: subscription.id,
      payment: paymentIntent.id,
      unusedAmount,
    });
    try {
      await doOverdraftProtection(paymentIntent, subscription, customer!, invoice);
      return updates.terminate;
    } catch (err) {
      logger.error('do overdraft protection failed', {
        error: err,
        subscription: subscription.id,
      });
    }
  }

  if (subscription.isImmutable()) {
    logger.info('Subscription is immutable, no need to check due', { subscription: subscription.id });
    return updates.terminate;
  }
  // check days until cancel
  const dueUnit = getDueUnit(interval);
  const daysUntilCancel = getDaysUntilCancel(subscription);
  const cancelUpdates: { [key: string]: any } = {};
  if (daysUntilCancel > 0) {
    cancelUpdates.cancel_at = subscription.current_period_start + daysUntilCancel * dueUnit;
  } else {
    cancelUpdates.cancel_at_period_end = true;
  }

  // check days until due
  const daysUntilDue = getDaysUntilDue(subscription);
  if (typeof daysUntilDue === 'number') {
    const gracePeriodStart = subscription.current_period_start;
    const graceDuration = daysUntilDue ? daysUntilDue * dueUnit : 0;
    logger.debug('handlePaymentFailed.checkDue', { now, daysUntilDue, dueUnit, gracePeriodStart, graceDuration });
    if (gracePeriodStart + graceDuration <= now) {
      await subscription.update({
        status: 'past_due',
        ...cancelUpdates,
        cancelation_details: {
          comment: 'past_due',
          feedback: 'other',
          reason: 'payment_failed',
        },
      });
      logger.warn('Subscription moved to past_due after payment failed', {
        subscription: subscription.id,
        payment: paymentIntent.id,
        cancelUpdates,
        gracePeriodStart,
        graceDuration,
        dueUnit,
      });
      return updates.terminate;
    }
  }

  // check max retry
  const maxRetry = getMaxRetryCount(interval);
  if (invoice.attempt_count >= maxRetry) {
    await subscription.update({
      status: 'past_due',
      ...cancelUpdates,
      cancelation_details: {
        comment: 'exceed_max_retry',
        feedback: 'other',
        reason: 'payment_failed',
      },
    });
    logger.warn('Subscription moved to past_due after max retry', {
      subscription: subscription.id,
      payment: paymentIntent.id,
      attempt_count: invoice.attempt_count,
    });

    if (invoice.billing_reason === 'auto_recharge' && invoice.metadata?.recharge_config?.recharge_id) {
      const autoRechargeConfig = await AutoRechargeConfig.findByPk(invoice.metadata?.recharge_config?.recharge_id);
      if (autoRechargeConfig && autoRechargeConfig.enabled) {
        autoRechargeConfig.update({
          enabled: false,
          metadata: {
            ...autoRechargeConfig.metadata,
            failReason: 'Payment failed',
          },
        });
      }
    }

    return updates.terminate;
  }

  return updates.retry;
};

const handleStakeSlash = async (
  invoice: Invoice,
  paymentIntent: PaymentIntent,
  paymentMethod: PaymentMethod,
  customer: Customer,
  paymentCurrency: PaymentCurrency
) => {
  const subscription = await Subscription.findByPk(invoice.subscription_id);
  if (!subscription) {
    logger.warn('Stake slashing skipped because Subscription not found', {
      subscription: invoice.subscription_id,
      paymentIntent: paymentIntent.id,
      invoice: invoice.id,
    });
    return;
  }
  if (!subscription.cancelation_details?.slash_stake || subscription.status !== 'canceled') {
    logger.warn('Stake slashing skipped because subscription not canceled or slash_stake is false', {
      subscription: invoice.subscription_id,
      paymentIntent: paymentIntent.id,
      invoice: invoice.id,
    });
    return;
  }

  const address = await getSubscriptionStakeAddress(subscription, customer.did, paymentMethod);
  const slashAmount = paymentIntent.amount;
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, slashAmount);
  if (!stakeEnough.enough) {
    logger.warn('Stake slashing aborted because no enough staking', {
      subscription: subscription.id,
      paymentIntent: paymentIntent.id,
      invoice: invoice.id,
      address,
      staked: stakeEnough.revoked,
      revoked: stakeEnough.revoked,
    });
    return;
  }
  if (slashAmount === '0') {
    logger.warn('Stake slashing aborted because slashAmount is 0', {
      subscription: subscription.id,
      paymentIntent: paymentIntent.id,
      invoice: invoice.id,
      address,
      slashAmount,
    });
    return;
  }

  // do the slash
  const client = paymentMethod.getOcapClient();
  const signed = await client.signSlashStakeTx({
    tx: {
      itx: {
        address,
        outputs: [{ owner: wallet.address, tokens: [{ address: paymentCurrency.contract, value: slashAmount }] }],
        message: 'stake_slash_on_subscription_cancel',
        data: {
          typeUrl: 'json',
          // @ts-ignore
          value: {
            appId: wallet.address,
            reason: 'stake_slash_on_subscription_cancel',
            subscriptionId: subscription.id,
            invoiceId: invoice.id,
            paymentIntentId: paymentIntent.id,
          },
        },
      },
    },
    wallet,
  });
  // @ts-ignore
  const { buffer } = await client.encodeSlashStakeTx({ tx: signed });
  // @ts-ignore
  const txHash = await client.sendSlashStakeTx({ tx: signed, wallet }, await getGasPayerExtra(buffer));
  logger.info('Stake slashing done', {
    subscription: subscription.id,
    amount: slashAmount,
    paymentIntent: paymentIntent.id,
    invoice: invoice.id,
    address,
    txHash,
  });
  await paymentIntent.update({
    status: 'succeeded',
    amount_received: slashAmount,
    payment_details: {
      arcblock: {
        tx_hash: txHash,
        payer: customer.did,
        type: 'slash',
      },
    },
  });
  await handlePaymentSucceed(paymentIntent, true, true);
  const jobId = `${paymentIntent.id}-${subscription.id}`;
  const job = await notificationQueue.get(jobId);
  if (job) {
    return;
  }
  notificationQueue.push({
    id: jobId,
    job: {
      type: 'subscription.stake.slash.succeeded',
      options: {
        paymentIntentId: paymentIntent.id,
        subscriptionId: subscription.id,
        invoiceId: invoice.id,
      },
    },
  });
};

export const handlePayment = async (job: PaymentJob) => {
  logger.info('handle payment', job);

  const paymentIntent = await PaymentIntent.findByPk(job.paymentIntentId);
  if (!paymentIntent) {
    logger.warn('PaymentIntent not found', { id: job.paymentIntentId });
    return;
  }

  if (['requires_capture', 'processing'].includes(paymentIntent.status) === false) {
    logger.warn('PaymentIntent status not expected', { id: paymentIntent.id, status: paymentIntent.status });
    return;
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
  if (!paymentMethod) {
    logger.warn('PaymentMethod not found', { id: paymentIntent.payment_method_id });
    return;
  }

  const supportAutoCharge = await PaymentMethod.supportAutoCharge(paymentIntent.payment_method_id);
  if (supportAutoCharge === false) {
    logger.warn('PaymentMethod does not support auto charge', { id: paymentIntent.payment_method_id });
    return;
  }

  const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
  if (!paymentCurrency) {
    logger.warn('PaymentCurrency not found', { id: paymentIntent.currency_id });
    return;
  }

  if (paymentCurrency.isCredit()) {
    logger.info('PaymentIntent capture skipped because paymentCurrency is credit', { id: paymentIntent.id });
    return;
  }

  const customer = await Customer.findByPk(paymentIntent.customer_id);
  if (!customer) {
    logger.warn('Customer not found', { id: paymentIntent.customer_id });
    return;
  }

  const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
  // Fetch subscription early for migration fallback support and overdraft protection check
  const subscription = invoice?.subscription_id ? await Subscription.findByPk(invoice.subscription_id) : null;

  if (invoice?.status === 'void') {
    await paymentIntent.update({
      status: 'canceled',
      canceled_at: dayjs().unix(),
      cancellation_reason: 'void_invoice',
    });
    logger.info('PaymentIntent capture skipped because invoice is void', { id: paymentIntent.id });
    return;
  }

  if (
    subscription &&
    subscription.isActive() &&
    subscription.overdraft_protection?.enabled &&
    invoice?.billing_reason === 'overdraft_protection'
  ) {
    logger.info('PaymentIntent capture skipped because of overdraft protection', {
      id: paymentIntent.id,
      subscription: subscription.id,
    });
    return;
  }

  // check max retry before doing any hard work
  if (invoice && invoice.attempt_count >= MAX_RETRY_COUNT && !job.ignoreMaxRetryCheck) {
    logger.info('PaymentIntent capture aborted since max retry exceeded', { id: paymentIntent.id });
    const updates = await handlePaymentFailed(
      paymentIntent,
      invoice,
      {
        type: 'card_error',
        code: 'max_retry_exceeded',
        message: 'max_retry_exceeded',
      },
      false
    );
    await paymentIntent.update(updates.payment);
    await invoice.update(updates.invoice);
    if (!updates.invoice.next_payment_attempt) {
      logger.info('PaymentIntent job deleted since max retry exceeded', { id: paymentIntent.id });
      await paymentQueue.delete(paymentIntent.id);
    }
    return;
  }

  const paymentSettings = invoice?.payment_settings || job.paymentSettings;
  if (!paymentSettings) {
    await paymentIntent.update({ status: 'requires_action' });
    logger.warn('Payment settings not found:', job);
    return;
  }

  // try payment capture and reschedule on error
  logger.info('PaymentIntent capture attempt', { id: paymentIntent.id, attempt: invoice?.attempt_count });
  let result;

  // Use lock to prevent race condition with subscription queue
  const lock = getLock(`payment-${paymentIntent.id}`);

  try {
    await lock.acquire();
    logger.debug('Acquired lock for payment processing', {
      paymentIntent: paymentIntent.id,
      invoice: invoice?.id,
    });

    // Re-check payment intent status after acquiring lock
    await paymentIntent.reload();
    if (paymentIntent.status === 'succeeded') {
      logger.info('PaymentIntent already succeeded, skipping', { id: paymentIntent.id });
      return;
    }

    if (paymentIntent.amount === '0') {
      await handlePaymentSucceed(paymentIntent);
      return;
    }

    // Queue delay monitoring - critical for quote lock window observability
    const queueDelayMs = Date.now() - new Date(paymentIntent.created_at).getTime();
    const QUOTE_LOCK_WINDOW_MS = 180000; // 180 seconds

    logger.info('Queue processing payment intent', {
      paymentIntentId: paymentIntent.id,
      queueDelayMs,
      delaySeconds: Math.round(queueDelayMs / 1000),
      lockWindowSeconds: QUOTE_LOCK_WINDOW_MS / 1000,
    });

    if (queueDelayMs > QUOTE_LOCK_WINDOW_MS) {
      logger.warn('CRITICAL: Queue delay exceeds quote lock window', {
        paymentIntentId: paymentIntent.id,
        invoiceId: paymentIntent.invoice_id,
        queueDelayMs,
        delaySeconds: Math.round(queueDelayMs / 1000),
        lockWindowSeconds: QUOTE_LOCK_WINDOW_MS / 1000,
        riskLevel: 'HIGH - Quote lock expired, slippage protection critical',
      });
    }

    const preQuoteValidation = await validateQuoteForPayment({
      checkoutSessionId: invoice?.checkout_session_id,
      invoiceId: paymentIntent.invoice_id,
      amount: paymentIntent.amount,
    });

    if (!preQuoteValidation.valid) {
      // Enhanced error logging with queue delay context
      let failureReason = 'QUOTE_VALIDATION_FAILED';
      if (preQuoteValidation.reason?.includes('QUOTE_MAX_PAYABLE_EXCEEDED')) {
        failureReason = 'SLIPPAGE_EXCEEDED';
      } else if (preQuoteValidation.reason?.includes('expired')) {
        failureReason = 'QUOTE_EXPIRED';
      }

      logger.error('Payment aborted due to quote validation before capture', {
        paymentIntentId: paymentIntent.id,
        invoiceId: paymentIntent.invoice_id,
        quoteId: preQuoteValidation.quoteId,
        reason: preQuoteValidation.reason,
        failureReason,
        queueDelayMs,
        delaySeconds: Math.round(queueDelayMs / 1000),
      });
      throw new CustomError(failureReason, `Payment validation failed: ${preQuoteValidation.reason || 'unknown'}`);
    }

    await paymentIntent.update({ status: 'processing', last_payment_error: null });

    if (paymentMethod.type === 'arcblock') {
      if (invoice?.billing_reason === 'slash_stake') {
        await handleStakeSlash(invoice, paymentIntent, paymentMethod, customer, paymentCurrency);
        return;
      }
      const client = paymentMethod.getOcapClient();

      const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;

      result = await isDelegationSufficientForPayment({
        paymentMethod,
        paymentCurrency,
        userDid: payer || customer.did,
        amount: paymentIntent.amount,
        subscription,
      });

      if (result.sufficient === false) {
        logger.error('PaymentIntent capture aborted - insufficient balance/delegation', {
          paymentIntentId: paymentIntent.id,
          invoiceId: paymentIntent.invoice_id,
          failureReason: result.reason,
          result,
          queueDelayMs,
          delaySeconds: Math.round(queueDelayMs / 1000),
        });
        throw new CustomError(result.reason, 'Payer balance or delegation not sufficient for this payment');
      }

      const signed = await client.signTransferV2Tx({
        tx: {
          itx: {
            to: wallet.address,
            value: '0',
            assets: [],
            tokens: [{ address: paymentCurrency.contract, value: paymentIntent.amount }],
            data: {
              typeUrl: 'json',
              // @ts-ignore
              value: {
                appId: wallet.address,
                reason: invoice ? invoice.billing_reason : 'payment',
                paymentIntentId: paymentIntent.id,
              },
            },
          },
        },
        wallet,
        delegator: result.delegator,
      });

      // @ts-ignore
      const { buffer } = await client.encodeTransferV2Tx({ tx: signed });

      const txHash = await client.sendTransferV2Tx(
        // @ts-ignore
        { tx: signed, wallet, delegator: result.delegator },
        await getGasPayerExtra(buffer)
      );
      logger.info('PaymentIntent txHash', { txHash });
      logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash });

      // CRITICAL: Validate quote before marking payment as succeeded
      const quoteValidation = await validateQuoteForPayment({
        checkoutSessionId: undefined, // Will be found through invoice if exists
        invoiceId: paymentIntent.invoice_id,
        amount: paymentIntent.amount,
      });

      if (!quoteValidation.valid) {
        if (quoteValidation.action === 'reject') {
          // Reject the payment - mark as failed
          logger.error('Automated payment rejected due to quote validation', {
            paymentIntentId: paymentIntent.id,
            invoiceId: paymentIntent.invoice_id,
            quoteId: quoteValidation.quoteId,
            reason: quoteValidation.reason,
            txHash,
          });
          throw new CustomError('quote_validation_failed', `Payment validation failed: ${quoteValidation.reason}`);
        }

        if (quoteValidation.action === 'require_manual_review') {
          // Hold for manual review
          logger.error('Automated payment requires manual review due to expired quote', {
            paymentIntentId: paymentIntent.id,
            invoiceId: paymentIntent.invoice_id,
            quoteId: quoteValidation.quoteId,
            reason: quoteValidation.reason,
            txHash,
          });

          await handleExpiredQuotePayment({
            quoteId: quoteValidation.quoteId!,
            checkoutSessionId: undefined,
            invoiceId: paymentIntent.invoice_id,
            paymentIntentId: paymentIntent.id,
            amount: paymentIntent.amount,
            currencyId: paymentIntent.currency_id,
            txHash,
          });

          throw new CustomError('quote_expired', `Payment held for manual review: ${quoteValidation.reason}`);
        }
      }

      await paymentIntent.update({
        status: 'succeeded',
        last_payment_error: null,
        amount_received: paymentIntent.amount,
        payment_details: {
          arcblock: {
            tx_hash: txHash,
            payer: result.delegator as string,
            type: 'transfer',
          },
        },
      });

      await handlePaymentSucceed(paymentIntent);
    }

    if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
      if (!paymentCurrency.contract) {
        throw new Error('Payment capture not supported for ethereum payment currencies without contract');
      }

      const client = paymentMethod.getEvmClient();
      const paymentType = paymentMethod.type;
      const payer = paymentSettings?.payment_method_options[paymentType as EVMChainType]?.payer as string;

      // check balance before capture with transaction
      result = await isDelegationSufficientForPayment({
        paymentMethod,
        paymentCurrency,
        userDid: payer as string,
        amount: paymentIntent.amount,
      });
      if (result.sufficient === false) {
        logger.error('PaymentIntent capture aborted - insufficient balance/delegation', {
          paymentIntentId: paymentIntent.id,
          invoiceId: paymentIntent.invoice_id,
          failureReason: 'INSUFFICIENT_BALANCE',
          result,
          queueDelayMs,
          delaySeconds: Math.round(queueDelayMs / 1000),
        });
        throw new CustomError('INSUFFICIENT_BALANCE', 'Payer balance or delegation not sufficient for this payment');
      }

      // do the capture
      const receipt = await transferErc20FromUser(client, paymentCurrency.contract, payer, paymentIntent.amount);
      logger.info('PaymentIntent capture done', { id: paymentIntent.id, txHash: receipt.hash });

      // CRITICAL: Validate quote before marking payment as succeeded
      const quoteValidation = await validateQuoteForPayment({
        checkoutSessionId: undefined, // Will be found through invoice if exists
        invoiceId: paymentIntent.invoice_id,
        amount: paymentIntent.amount,
      });

      if (!quoteValidation.valid) {
        if (quoteValidation.action === 'reject') {
          // Reject the payment - mark as failed
          logger.error('Automated EVM payment rejected due to quote validation', {
            paymentIntentId: paymentIntent.id,
            invoiceId: paymentIntent.invoice_id,
            quoteId: quoteValidation.quoteId,
            reason: quoteValidation.reason,
            txHash: receipt.hash,
          });
          throw new CustomError('quote_validation_failed', `Payment validation failed: ${quoteValidation.reason}`);
        }

        if (quoteValidation.action === 'require_manual_review') {
          // Hold for manual review
          logger.error('Automated EVM payment requires manual review due to expired quote', {
            paymentIntentId: paymentIntent.id,
            invoiceId: paymentIntent.invoice_id,
            quoteId: quoteValidation.quoteId,
            reason: quoteValidation.reason,
            txHash: receipt.hash,
          });

          await handleExpiredQuotePayment({
            quoteId: quoteValidation.quoteId!,
            checkoutSessionId: undefined,
            invoiceId: paymentIntent.invoice_id,
            paymentIntentId: paymentIntent.id,
            amount: paymentIntent.amount,
            currencyId: paymentIntent.currency_id,
            txHash: receipt.hash,
          });

          throw new CustomError('quote_expired', `Payment held for manual review: ${quoteValidation.reason}`);
        }
      }

      await paymentIntent.update({
        status: 'succeeded',
        last_payment_error: null,
        amount_received: paymentIntent.amount,
        payment_details: {
          [paymentType]: {
            tx_hash: receipt.hash,
            payer: payer as string,
            block_height: receipt.blockNumber.toString(),
            gas_used: receipt.gasUsed.toString(),
            gas_price: receipt.gasPrice.toString(),
            type: 'transfer',
          },
        },
      });

      await handlePaymentSucceed(paymentIntent, true);
    }
  } catch (err) {
    logger.error('PaymentIntent capture failed', { error: err, id: paymentIntent.id });

    const error: PaymentError = {
      type: 'card_error',
      code: err.code,
      message: err.message,
      payment_settings: paymentSettings,
      payment_method_id: paymentMethod.id,
      payment_method_type: paymentMethod.type,
    };

    if (!job.retryOnError) {
      //  To a final state without any retry
      await paymentIntent.update({ status: 'requires_action', last_payment_error: error });
      if (invoice) {
        await invoice.update({
          status: 'uncollectible',
          attempt_count: invoice.attempt_count + 1,
          attempted: true,
          status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
        });
      }
    } else if (invoice) {
      // This means we have tried to capture this invoice before, since the retry is managed by invoice queue
      const attemptCount = invoice.attempt_count + 1;

      const updates = await handlePaymentFailed(paymentIntent, invoice, error);
      await paymentIntent.update(updates.payment);
      await invoice.update(updates.invoice);

      // 只有在 第一次重试 或者 重试次数超过阈值 的时候才发送邮件，不然邮件频率太高了
      const minRetryMail = updates.minRetryMail || MIN_RETRY_MAIL;
      const needSendMail = attemptCount === 1 || attemptCount >= minRetryMail;

      logger.warn('catch:PaymentIntent capture failed', { id: paymentIntent.id, attemptCount, minRetryMail, invoice });

      if (needSendMail && invoice.billing_reason === 'subscription_cycle') {
        if (subscription) {
          await subscription.update({
            metadata: Object.assign(subscription.metadata, {
              renew_failed_reason: result || { sufficient: false, reason: 'TX_SEND_FAILED' },
            }),
          });
          logger.info('createEvent:customer.subscription.renew_failed', {
            subscriptionId: subscription.id,
            invoiceId: invoice.id,
          });
          if (!subscription.isImmutable()) {
            createEvent('Subscription', 'customer.subscription.renew_failed', subscription, {
              invoiceId: invoice.id,
            });
          }
        }
      }

      if (needSendMail && invoice.billing_reason === 'auto_recharge') {
        await invoice.update({
          metadata: {
            ...invoice.metadata,
            auto_recharge_failed_reason: result || { sufficient: false, reason: 'TX_SEND_FAILED' },
          },
        });
        createEvent('Customer', 'customer.auto_recharge.failed', customer, {
          autoRechargeConfigId: invoice.metadata?.auto_recharge?.config_id,
          invoiceId: invoice.id,
          paymentIntentId: paymentIntent.id,
        });
      }
      // reschedule next attempt
      const retryAt = updates.invoice.next_payment_attempt;
      if (retryAt) {
        paymentQueue.push({
          id: paymentIntent.id,
          job: { paymentIntentId: paymentIntent.id, retryOnError: job.retryOnError },
          runAt: retryAt,
        });
        logger.error('PaymentIntent capture retry scheduled', { id: paymentIntent.id, retryAt });
      } else {
        logger.info('PaymentIntent job deleted since no retry', { id: paymentIntent.id });
        paymentQueue.delete(paymentIntent.id);
      }
    }
  } finally {
    await lock.release();
  }
};

export const paymentQueue = createQueue<PaymentJob>({
  name: 'payment',
  onJob: handlePayment,
  options: {
    concurrency: 1,
    maxRetries: 0,
    enableScheduledJob: true,
  },
});

export const startPaymentQueue = async () => {
  const payments = await PaymentIntent.findAll({
    where: {
      status: ['requires_capture', 'processing'],
      capture_method: 'automatic',
    },
  });

  payments.forEach(async (x) => {
    const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.payment_method_id);
    if (supportAutoCharge === false) {
      return;
    }
    const exist = await paymentQueue.get(x.id);
    if (!exist) {
      paymentQueue.push({ id: x.id, job: { paymentIntentId: x.id } });
    }
  });
};

paymentQueue.on('failed', ({ id, job, error }) => {
  logger.error('Payment job failed', { id, job, error });
});

// Do gas stake as soon as possible after payment succeed
events.on('payment_intent.succeeded', async (paymentIntent: PaymentIntent) => {
  const paymentMethod = await PaymentMethod.findByPk(paymentIntent.payment_method_id);
  if (paymentMethod && paymentMethod.type === 'arcblock') {
    ensureStakedForGas();
  }
});

events.on('payment.queued', async (id, job, args = {}) => {
  const { sync, ...extraArgs } = args;
  if (sync) {
    try {
      await paymentQueue.pushAndWait({
        id,
        job,
        ...extraArgs,
      });
      events.emit('payment.queued.done', { id, job });
    } catch (error) {
      logger.error('Error in payment.queued', { id, job, error });
      events.emit('payment.queued.error', { id, job, error });
    }
    return;
  }

  try {
    const existingJob = await paymentQueue.get(id);
    if (existingJob) {
      await paymentQueue.delete(id);
      logger.info('Removed existing payment job for immediate execution', {
        id,
        originalRunAt: existingJob.runAt,
      });
    }
    paymentQueue.push({
      id,
      job,
      ...extraArgs,
    });
    events.emit('payment.queued.done', { id, job });
  } catch (error) {
    logger.error('Error in payment.queued', { id, job, error });
    events.emit('payment.queued.error', { id, job, error });
  }
});

export async function startDepositVaultQueue() {
  logger.debug('startDepositVaultQueue');
  const paymentCurrencies = (await PaymentCurrency.scope('withVaultConfig').findAll({
    include: [{ model: PaymentMethod, as: 'payment_method' }],
  })) as (PaymentCurrency & { payment_method: PaymentMethod })[];
  await pAll(
    paymentCurrencies.map((x) => {
      if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(x.payment_method.type) && x.vault_config?.enabled === true) {
        depositVaultQueue.push({ id: `deposit-vault-${x.id}`, job: { currencyId: x.id } });
      }
      return async () => {};
    }),
    {
      concurrency: 5,
    }
  );
}
