import type { LiteralUnion } from 'type-fest';

import { BN } from '@ocap/util';
import { Op } from 'sequelize';
import { createEvent } from '../libs/audit';
import { ensurePassportRevoked } from '../integrations/blocklet/passport';
import { batchHandleStripeSubscriptions } from '../integrations/stripe/resource';
import { wallet } from '../libs/auth';
import { getExchangeRateService } from '../libs/exchange-rate';
import dayjs from '../libs/dayjs';
import { events } from '../libs/event';
import { getLock } from '../libs/lock';
import logger from '../libs/logger';
import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
import createQueue from '../libs/queue';
import { getStatementDescriptor } from '../libs/session';
import { NonRetryableError } from '../libs/error';
import {
  checkRemainingStake,
  checkUsageReportEmpty,
  getSubscriptionCycleAmount,
  getSubscriptionCycleSetup,
  getSubscriptionPaymentAddress,
  getSubscriptionRefundSetup,
  getSubscriptionStakeAddress,
  getSubscriptionStakeReturnSetup,
  getSubscriptionStakeSlashSetup,
  isSubscriptionOverdraftProtectionEnabled,
  returnOverdraftProtectionStake,
  shouldCancelSubscription,
  slashOverdraftProtectionStake,
} from '../libs/subscription';
import { resetPeriodGrantCounter } from '../libs/credit-schedule';
import { ensureInvoiceAndItems, migrateSubscriptionPaymentMethodInvoice } from '../libs/invoice';
import { getQuoteService } from '../libs/quote-service';
import { isRateBelowMinAcceptableRate } from '../libs/slippage';
import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, SetupIntent, UsageRecord } from '../store/models';
import { Customer } from '../store/models/customer';
import { Invoice, nextInvoiceId } from '../store/models/invoice';
import { Price } from '../store/models/price';
import { Subscription } from '../store/models/subscription';
import { SubscriptionItem } from '../store/models/subscription-item';
import { getValidDiscountsForSubscriptionBilling } from '../libs/discount/coupon';
import { invoiceQueue } from './invoice';
import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
import { applySubscriptionDiscount } from '../libs/discount/discount';
import type { ChainType, TLineItemExpanded } from '../store/models';
import { getExchangeRateSymbol } from '../libs/exchange-rate/token-address-mapping';

type SubscriptionJob = {
  subscriptionId: string;
  action?: LiteralUnion<'cycle' | 'cancel' | 'pause' | 'resume', string>;
};

const EXPECTED_SUBSCRIPTION_STATUS = ['trialing', 'active', 'paused', 'past_due'];

async function attachQuotesForInvoice(
  lineItems: TLineItemExpanded[],
  currencyId: string,
  invoiceId: string,
  slippageConfig?: { percent?: number; min_acceptable_rate?: string }
): Promise<{
  lineItems: TLineItemExpanded[];
  quotes: any[];
  slippageCheck?: { belowThreshold: boolean; currentRate?: string; minAcceptableRate?: string };
}> {
  // Filter dynamic items, excluding items with zero or negative quantity
  // This matches the logic in invoice-quote.ts:generateQuotesForInvoiceItems
  // to ensure quote creation is consistent with actual payment amount
  const dynamicItems = lineItems.filter((item) => {
    const price = (item as any)?.upsell_price || (item as any)?.price;
    if (price?.pricing_type !== 'dynamic') {
      return false;
    }
    // Skip items with zero or negative quantity (no usage, no payment needed)
    if (item.quantity <= 0) {
      return false;
    }
    return true;
  });
  if (!dynamicItems.length) {
    return { lineItems, quotes: [] };
  }

  const currency = (await PaymentCurrency.findByPk(currencyId, {
    include: [{ model: PaymentMethod, as: 'payment_method' }],
  })) as PaymentCurrency & { payment_method: PaymentMethod };
  if (!currency) {
    throw new NonRetryableError('CURRENCY_NOT_FOUND', `Currency ${currencyId} not found`);
  }

  if (currency.payment_method?.type === 'stripe') {
    return { lineItems, quotes: [] };
  }

  const exchangeRateService = getExchangeRateService();
  const quoteService = getQuoteService();
  // For ArcBlock payment method, always use ABT for exchange rate
  const rateSymbol = getExchangeRateSymbol(currency.symbol, currency.payment_method?.type as ChainType);
  const rateResult = await exchangeRateService.getRate(rateSymbol);

  const rateBelowSlippage = isRateBelowMinAcceptableRate(rateResult.rate, slippageConfig?.min_acceptable_rate);

  const quoteResults = await Promise.all(
    dynamicItems.map(async (item) => {
      const targetPrice: any = (item as any).upsell_price || (item as any).price;
      const quoteResponse = await quoteService.createQuoteWithRate({
        price_id: targetPrice.id,
        invoice_id: invoiceId,
        target_currency_id: currencyId,
        quantity: item.quantity,
        rateResult,
        slippage_percent: slippageConfig?.percent,
        min_acceptable_rate: undefined,
      });
      return { item, quoteResponse };
    })
  );

  const quoteMap = new Map<string, (typeof quoteResults)[number]>();
  quoteResults.forEach((result) => {
    const targetPrice: any = (result.item as any).upsell_price || (result.item as any).price;
    quoteMap.set(targetPrice.id, result);
  });

  const enriched = lineItems.map((item) => {
    const targetPrice: any = (item as any).upsell_price || (item as any).price;
    const hit = quoteMap.get(targetPrice?.id);
    if (!hit) {
      return item;
    }
    const { quoteResponse } = hit;
    return {
      ...item,
      quote_id: quoteResponse.quote.id,
      quoted_amount: quoteResponse.computed_unit_amount,
      expires_at: quoteResponse.expires_at,
      exchange_rate: quoteResponse.quote.exchange_rate,
      rate_provider_name: quoteResponse.quote.rate_provider_name,
      rate_provider_id: quoteResponse.quote.rate_provider_id,
      custom_amount: quoteResponse.computed_unit_amount,
    } as any;
  });

  return {
    lineItems: enriched,
    quotes: quoteResults.map((x) => x.quoteResponse.quote),
    slippageCheck: {
      belowThreshold: rateBelowSlippage,
      currentRate: rateResult.rate,
      minAcceptableRate: slippageConfig?.min_acceptable_rate,
    },
  };
}

const doHandleSubscriptionInvoice = async ({
  subscription,
  filter,
  status,
  reason,
  start,
  end,
  offset,
  usageStart,
  usageEnd,
  metadata,
}: {
  subscription: Subscription;
  filter: (x: any) => boolean;
  status: string;
  reason: 'cycle' | 'cancel' | 'threshold' | 'recover';
  start: number;
  end: number;
  offset: number;
  usageStart?: number;
  usageEnd?: number;
  metadata?: Record<string, any>;
}) => {
  // Do we still have the customer
  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    logger.warn(`Customer ${subscription.customer_id} not found for subscription: ${subscription.id}`);
    return null;
  }

  // Do we still have the currency
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!currency) {
    logger.warn(`Currency ${subscription.currency_id} not found for subscription: ${subscription.id}`);
    return null;
  }

  // Do not proceed if previous invoice is not immutable
  if (reason === 'cycle') {
    const previous = await Invoice.findOne({
      where: {
        subscription_id: subscription.id,
        period_start: subscription.current_period_start,
        period_end: subscription.current_period_end,
        billing_reason: `subscription_${reason}`,
      },
    });
    let existOverdraftProtection;
    if (previous) {
      existOverdraftProtection = await Invoice.findOne({
        where: {
          subscription_id: subscription.id,
          billing_reason: 'overdraft_protection',
          'metadata.invoice_id': previous.id,
        },
      });
    }

    if (previous && previous.isImmutable() === false && !existOverdraftProtection) {
      logger.warn(`Invoice for current period ${previous.id} not paid for subscription ${subscription.id}`);
      return null;
    }
  }

  // check if invoice already created for this reason
  if (['cycle', 'cancel', 'recover'].includes(reason)) {
    const exist = await Invoice.findOne({
      where: {
        subscription_id: subscription.id,
        period_start: start,
        period_end: end,
        billing_reason: `subscription_${reason}`,
      },
    });
    if (exist) {
      logger.warn(`Invoice already created for subscription ${subscription.id} for ${reason}: ${exist.id}`);
      return null;
    }
  }

  // expand subscription items
  const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } });
  let expandedItems = await Price.expand(
    subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity })),
    { product: true }
  );

  const usageReportStart = usageStart || start - offset;
  const usageReportEnd = usageEnd || end - offset;

  if (subscription.status !== 'trialing' && reason !== 'recover') {
    // check if usage report is empty
    const usageReportEmpty = await checkUsageReportEmpty(subscription, usageReportStart, usageReportEnd);
    if (usageReportEmpty) {
      createEvent('Subscription', 'usage.report.empty', subscription, {
        usageReportStart,
        usageReportEnd,
      }).catch(console.error);
      logger.info('create usage report empty event', {
        subscriptionId: subscription.id,
        usageReportStart,
        usageReportEnd,
      });
    }
  }

  // get usage summaries for this billing cycle
  expandedItems = await Promise.all(
    expandedItems.filter(filter).map(async (x: any) => {
      // For metered billing, we need to get usage summary for this billing cycle
      // @link https://stripe.com/docs/products-prices/pricing-models#usage-types
      if (x.price.recurring?.usage_type === 'metered') {
        const rawQuantity = await UsageRecord.getSummary({
          id: x.id,
          start: usageReportStart,
          end: usageReportEnd,
          method: x.price.recurring?.aggregate_usage,
          dryRun: false,
        });

        x.quantity = x.price.transformQuantity(rawQuantity);

        logger.info('Invoice.usageRecordSummary', {
          subscriptionId: subscription.id,
          subscriptionItemId: x.id,
          priceId: x.price_id,
          transformQuantity: x.price.transform_quantity,
          rawQuantity,
          quantity: x.quantity,
          start: (usageStart || start) - offset,
          end: (usageEnd || end) - offset,
          usage: x.price.recurring?.aggregate_usage,
        });

        // record raw quantity in metadata
        x.metadata = x.metadata || {};
        x.metadata.quantity = rawQuantity;
      }

      return x;
    })
  );
  if (expandedItems.length === 0) {
    return null;
  }

  const invoiceId = nextInvoiceId();
  // Get slippage config from subscription
  const slippageConfig = subscription.slippage_config as { percent?: number; min_acceptable_rate?: string } | undefined;
  const {
    lineItems: itemsWithQuotes,
    quotes,
    slippageCheck,
  } = await attachQuotesForInvoice(
    expandedItems as TLineItemExpanded[],
    subscription.currency_id,
    invoiceId,
    slippageConfig
  );
  expandedItems = itemsWithQuotes as any[];

  // Get valid discounts for this subscription billing period
  const { validDiscounts, expiredDiscounts } = await getValidDiscountsForSubscriptionBilling({
    subscriptionId: subscription.id,
    customerId: subscription.customer_id,
  });

  logger.info('Valid discounts for subscription billing', {
    subscriptionId: subscription.id,
    validDiscounts: validDiscounts.map((d) => d.id),
  });

  // Log expired discounts for monitoring
  if (expiredDiscounts.length > 0) {
    logger.info('Found expired discounts during subscription billing', {
      subscriptionId: subscription.id,
      expiredDiscounts: expiredDiscounts.map((d) => d.id),
    });
  }

  // Calculate subscription amount with discounts
  const baseAmount = await getSubscriptionCycleAmount(expandedItems, currency.id);

  // Apply discounts - simplified to use only one discount (first valid one)
  const firstValidDiscount = validDiscounts?.[0] || null;
  logger.info('First valid discount', {
    subscriptionId: subscription.id,
    firstValidDiscount: firstValidDiscount?.id,
  });
  let enhancedLineItems = expandedItems;
  let discountSummary: {
    appliedCoupon: string | null;
    discountAmount: string;
    totalDiscountAmount: string;
    finalTotal: string;
  } = {
    appliedCoupon: null,
    discountAmount: '0',
    totalDiscountAmount: '0',
    finalTotal: baseAmount.total,
  };

  if (firstValidDiscount) {
    // Check if subscription is still in trial period for accurate discount calculation
    const now = Math.floor(Date.now() / 1000);
    const isTrialing = !!(subscription.trial_end && subscription.trial_end > now);

    const discountApplication = await applySubscriptionDiscount({
      lineItems: expandedItems,
      discount: firstValidDiscount,
      currency,
      totalAmount: baseAmount.total,
      billingContext: {
        trialing: isTrialing,
      },
    });

    logger.info('Discount application', {
      subscriptionId: subscription.id,
      discountApplication,
    });

    enhancedLineItems = discountApplication.enhancedLineItems;
    discountSummary = discountApplication.discountSummary;
  }

  const { appliedCoupon, totalDiscountAmount, finalTotal } = discountSummary;

  // Additional safety check to ensure final total is never negative
  const safeFinalTotal = new BN(finalTotal || '0').lt(new BN('0')) ? '0' : finalTotal || '0';

  // Prepare data for invoice creation
  const appliedDiscounts = appliedCoupon && firstValidDiscount ? [firstValidDiscount.id] : [];
  const discountBreakdownForInvoice =
    appliedCoupon && firstValidDiscount ? [{ amount: totalDiscountAmount, discount: firstValidDiscount.id }] : [];

  if (validDiscounts.length > 0) {
    logger.info('Subscription billing discount calculation completed', {
      subscriptionId: subscription.id,
      originalTotal: baseAmount.total,
      totalDiscount: totalDiscountAmount,
      finalTotal,
      appliedDiscounts: appliedDiscounts.length,
    });
  }

  // Skip slippage check when final amount is 0 (e.g. 100% discount) — no crypto payment needed
  const isFinalAmountZero = new BN(safeFinalTotal).isZero();
  const shouldMarkOverdue = slippageCheck?.belowThreshold === true && !isFinalAmountZero;
  const invoiceStatus = shouldMarkOverdue && status === 'open' ? 'uncollectible' : status;

  const { invoice } = await ensureInvoiceAndItems({
    customer,
    currency,
    subscription,
    trialing: false,
    metered: true,
    lineItems: enhancedLineItems,
    props: {
      id: invoiceId,
      livemode: subscription.livemode,
      description: `Subscription ${reason}`,
      statement_descriptor: getStatementDescriptor(expandedItems),
      period_start: start,
      period_end: end,
      auto_advance: true,
      status: invoiceStatus,
      billing_reason: `subscription_${reason}`,
      currency_id: subscription.currency_id,
      quote_id: quotes[0]?.id,
      // Set correct subtotal (original amount) and total (after discount)
      subtotal: baseAmount.total,
      total: safeFinalTotal,
      payment_settings: subscription.payment_settings,
      default_payment_method_id: subscription.default_payment_method_id,
      // Discount information for Invoice
      discounts: appliedDiscounts,
      total_discount_amounts: discountBreakdownForInvoice,
      metadata: {
        ...metadata,
        ...(slippageCheck
          ? {
              slippage: {
                below_threshold: slippageCheck.belowThreshold,
                min_acceptable_rate: slippageCheck.minAcceptableRate,
                rate_at_invoice: slippageCheck.currentRate,
              },
            }
          : {}),
        quote_ids: quotes.map((q) => q.id),
      },
    } as unknown as Invoice,
  });

  // When slippage exceeded, create PaymentIntent with requires_action status for manual payment
  if (shouldMarkOverdue && invoice) {
    const paymentIntent = await PaymentIntent.create({
      livemode: !!invoice.livemode,
      amount: invoice.amount_remaining || safeFinalTotal,
      amount_received: '0',
      amount_capturable: '0',
      customer_id: invoice.customer_id,
      description: 'Slippage exceeded - manual payment required',
      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,
      statement_descriptor: invoice.statement_descriptor,
      statement_descriptor_suffix: '',
      setup_future_usage: 'on_session',
      metadata: {
        slippage_exceeded: true,
        current_rate: slippageCheck?.currentRate,
        min_acceptable_rate: slippageCheck?.minAcceptableRate,
      },
    });

    await invoice.update({ payment_intent_id: paymentIntent.id });

    logger.info('PaymentIntent created for slippage exceeded invoice', {
      invoiceId: invoice.id,
      paymentIntentId: paymentIntent.id,
      amount: paymentIntent.amount,
      currentRate: slippageCheck?.currentRate,
      minAcceptableRate: slippageCheck?.minAcceptableRate,
    });

    // Send notification for slippage exceeded
    events.emit('subscription.slippage_exceeded', subscription, {
      invoiceId: invoice.id,
      paymentIntentId: paymentIntent.id,
      currentRate: slippageCheck?.currentRate,
      minAcceptableRate: slippageCheck?.minAcceptableRate,
    });
  }

  if (shouldMarkOverdue && !['past_due', 'canceled', 'incomplete_expired'].includes(subscription.status)) {
    await subscription.update({
      status: 'past_due',
      cancelation_details: {
        comment: 'past_due',
        feedback: 'other',
        reason: 'slippage_below_threshold',
      },
    });
    logger.warn('Subscription moved to past_due due to slippage threshold', {
      subscription: subscription.id,
      invoice: invoice.id,
      currentRate: slippageCheck?.currentRate,
      minAcceptableRate: slippageCheck?.minAcceptableRate,
    });
  }

  logger.info('Invoice created for subscription', { invoice: invoice.id, subscription: subscription.id });

  return invoice;
};

export async function handleSubscriptionInvoice(args: Parameters<typeof doHandleSubscriptionInvoice>[0]) {
  const lock = getLock(`${args.subscription.id}-invoice`);
  await lock.acquire();
  try {
    const result = await doHandleSubscriptionInvoice(args);
    return result;
  } catch (error: any) {
    // Log non-retryable errors with more context
    if (error instanceof NonRetryableError) {
      logger.error('Non-retryable error in subscription invoice generation', {
        subscriptionId: args.subscription.id,
        errorCode: error.code,
        errorMessage: error.message,
        reason: args.reason,
        periodStart: args.start,
        periodEnd: args.end,
      });
    }
    throw error;
  } finally {
    // Always release lock, even if error occurs
    lock.release();
  }
}

const handleSubscriptionBeforeCancel = async (subscription: Subscription) => {
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    logger.warn('Payment currency not found for subscription', { subscription: subscription.id });
    return;
  }
  if (paymentCurrency.isCredit()) {
    logger.info('Skip invoice creation for credit subscription', { subscription: subscription.id });
    return;
  }
  const invoice = await handleSubscriptionInvoice({
    subscription,
    filter: (x) => x.price.recurring?.usage_type === 'metered', // include only metered items
    status: 'open',
    reason: 'cancel',
    start: subscription.current_period_start as number,
    end: subscription.current_period_end as number,
    offset: 0,
  });

  if (invoice) {
    // Just schedule the invoice processing, don't wait for completion
    // The actual payment will be handled by handleFinalInvoicePayment during stake operations
    await invoiceQueue.pushAndWait({
      id: invoice.id,
      job: { invoiceId: invoice.id, retryOnError: false },
    });

    logger.info('Final invoice job scheduled for async processing', {
      invoice: invoice.id,
      subscription: subscription.id,
    });

    // persist invoice id
    await subscription.update({ latest_invoice_id: invoice.id });
    logger.info('Subscription updated before cancel', {
      subscription: subscription.id,
      latestInvoiceId: invoice.id,
    });
  }
};

const handleSubscriptionWhenActive = async (subscription: Subscription) => {
  // get setup for next subscription period
  const previousPeriodEnd =
    subscription.status === 'trialing' ? subscription.trial_end : subscription.current_period_end;
  const previousPeriodStart = subscription.current_period_start;
  const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, previousPeriodEnd as number);

  // Check if this is a credit subscription
  const isCredit = await subscription.isConsumesCredit();
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);

  if (isCredit && paymentCurrency?.isCredit()) {
    // For credit subscriptions, check credit availability instead of creating invoices
    const customer = await Customer.findByPk(subscription.customer_id);
    const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);

    if (!customer || !paymentMethod || !paymentCurrency) {
      logger.warn('Credit subscription cycle skipped due to missing dependencies', {
        subscription: subscription.id,
        customer: !!customer,
        paymentMethod: !!paymentMethod,
        paymentCurrency: !!paymentCurrency,
      });
      return;
    }

    if (subscription.status === 'trialing') {
      const now = dayjs().unix();
      if (subscription.trial_end && subscription.trial_end <= now) {
        await subscription.update({ status: 'active' });
        logger.info('Subscription status updated from trialing to active', {
          subscription: subscription.id,
        });
      }
    }
    // Check if we need to catch up on missed periods
    const now = dayjs().unix();
    let nextPeriod = setup;

    if (now > setup.period.end) {
      // Enable catch-up for missed periods
      nextPeriod = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, previousPeriodEnd as number, {
        catchUp: true,
      });

      if (nextPeriod.missedPeriods > 0) {
        logger.warn(`Credit subscription recovered with ${nextPeriod.missedPeriods} missed periods`, {
          subscription: subscription.id,
          missedPeriods: nextPeriod.missedPeriods,
          originalPeriodEnd: nextPeriod.recovery?.originalPeriodEnd,
          currentPeriodStart: nextPeriod.period.start,
          currentPeriodEnd: nextPeriod.period.end,
          wasLimited: nextPeriod.recovery?.wasLimited,
        });
      }
    }

    await subscription.update({
      current_period_start: nextPeriod.period.start,
      current_period_end: nextPeriod.period.end,
    });
    if (subscription.credit_schedule_state && previousPeriodStart !== nextPeriod.period.start) {
      await resetPeriodGrantCounter(subscription);
    }

    if (subscription.isActive()) {
      logger.info(`Credit subscription updated for billing cycle: ${subscription.id}`, {
        periodStart: nextPeriod.period.start,
        periodEnd: nextPeriod.period.end,
        missedPeriods: nextPeriod.missedPeriods || 0,
      });
      await addSubscriptionJob(subscription, 'cycle', false, nextPeriod.period.end);
      logger.info(`Credit subscription job scheduled for next billing cycle: ${subscription.id}`);
    }

    return;
  }

  // Original logic for non-credit subscriptions
  // set invoice status if subscription paused
  let status = 'open';
  if (subscription.pause_collection) {
    if (subscription.pause_collection.behavior === 'mark_uncollectible') {
      status = 'uncollectible';
    }
    if (subscription.pause_collection.behavior === 'void') {
      status = 'void';
    }
    if (subscription.pause_collection.behavior === 'keep_as_draft') {
      status = 'draft';
    }
  }

  // change to active if still in trialing
  if (subscription.status === 'trialing') {
    const now = dayjs().unix();
    if (subscription.trial_end && subscription.trial_end <= now) {
      await subscription.update({ status: 'active' });
      logger.info('Subscription status updated from trialing to active', {
        subscription: subscription.id,
      });
    }
  }

  const invoice = await handleSubscriptionInvoice({
    subscription,
    filter: () => true, // include all items
    status,
    reason: 'cycle',
    start: setup.period.start,
    end: setup.period.end,
    offset: setup.cycle / 1000,
  });

  if (invoice) {
    // schedule invoice job
    invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
    logger.info(`Invoice job scheduled for new billing cycle: ${invoice.id}`);

    // persist invoice id
    await subscription.update({
      latest_invoice_id: invoice.id,
      current_period_start: setup.period.start,
      current_period_end: setup.period.end,
    });
    if (subscription.credit_schedule_state && previousPeriodStart !== setup.period.start) {
      await resetPeriodGrantCounter(subscription);
    }
    logger.info(`Subscription updated for new billing cycle: ${subscription.id}`);
  }

  // schedule next billing cycle if we are not in terminal state
  if (subscription.isActive()) {
    await addSubscriptionJob(subscription, 'cycle', false, setup.period.end);
    logger.info(`Subscription job scheduled for next billing cycle: ${subscription.id}`);
  }
};

const handleSubscriptionAfterRecover = async (subscription: Subscription) => {
  const invoice = await handleSubscriptionInvoice({
    subscription,
    filter: () => true, // include all items
    status: 'open',
    reason: 'recover',
    start: subscription.current_period_start,
    end: subscription.current_period_end,
    offset: 0,
  });

  if (invoice) {
    // schedule invoice job
    invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: true } });
    logger.info(`Invoice job scheduled for initial billing cycle after recover: ${invoice.id}`);

    // persist invoice id
    await subscription.update({ latest_invoice_id: invoice.id });
    logger.info(`Subscription updated for initial billing cycle after recover: ${subscription.id}`);
  }

  // schedule next billing cycle
  await addSubscriptionJob(subscription, 'cycle', false, subscription.current_period_end);
  logger.info(`Subscription job scheduled for next billing cycle after recover: ${subscription.id}`);
};

/**
 * Handle final metered invoice payment before stake operations
 * Checks user balance and slashes stake if insufficient funds
 */
const handleFinalInvoicePayment = async (
  subscription: Subscription,
  paymentMethod: PaymentMethod,
  paymentCurrency: PaymentCurrency
) => {
  // Check if there's any unpaid final metered invoice
  const lastInvoice = await Invoice.findOne({
    where: {
      subscription_id: subscription.id,
      billing_reason: 'subscription_cancel',
      status: { [Op.in]: ['open', 'uncollectible'] },
      amount_remaining: { [Op.gt]: '0' },
    },
    order: [['created_at', 'DESC']],
  });

  if (!lastInvoice) {
    logger.info('No unpaid final invoice found, skipping payment handling', {
      subscription: subscription.id,
    });
    return;
  }

  // Check payment status and handle accordingly
  const paymentIntent = await PaymentIntent.findByPk(lastInvoice.payment_intent_id);
  if (!paymentIntent) {
    logger.warn('PaymentIntent not found for final invoice', {
      subscription: subscription.id,
      invoice: lastInvoice.id,
    });
    return;
  }

  // If payment already succeeded, skip processing
  if (paymentIntent.status === 'succeeded') {
    logger.info('Final invoice already paid successfully, skipping', {
      subscription: subscription.id,
      invoice: lastInvoice.id,
      paymentIntentStatus: paymentIntent.status,
    });
    return;
  }

  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    logger.warn('Final invoice settlement skipped because customer not found', {
      subscription: subscription.id,
      invoice: lastInvoice.id,
    });
    return;
  }

  logger.info('Found unpaid final invoice, checking user delegation balance', {
    subscription: subscription.id,
    invoice: lastInvoice.id,
    amount: lastInvoice.amount_remaining,
  });

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

  const hasSufficientBalance = await isDelegationSufficientForPayment({
    paymentMethod,
    paymentCurrency,
    userDid: payer || customer.did,
    amount: lastInvoice.amount_remaining,
    subscription,
  });

  if (!hasSufficientBalance.sufficient) {
    logger.info('User has insufficient balance, slashing stake to pay final invoice', {
      subscription: subscription.id,
      invoice: lastInvoice.id,
      userBalance: hasSufficientBalance,
    });

    await handleStakeSlashAfterCancel(subscription, true);

    logger.info('Stake slashed for final invoice, proceeding with remaining stake return');
  }
};

export const handleStakeSlashAfterCancel = async (subscription: Subscription, forceSlash: boolean = false) => {
  const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
  if (!invoice || (invoice.status !== 'uncollectible' && !forceSlash)) {
    logger.warn('Stake slashing aborted because invoice status', {
      subscription: subscription.id,
      invoice: invoice?.id,
      status: invoice?.status,
    });
    return;
  }
  const currency = await PaymentCurrency.findByPk(invoice.currency_id || subscription.currency_id);
  if (!currency) {
    logger.warn('Stake slashing aborted because currency not found', {
      subscription: subscription.id,
      invoice: invoice.id,
      currency: invoice.currency_id || subscription.currency_id,
    });
    return;
  }
  const method = await PaymentMethod.findByPk(currency.payment_method_id);
  if (!method || method.type !== 'arcblock') {
    logger.warn('Stake slashing aborted because payment method not arcblock', {
      subscription: subscription.id,
      invoice: invoice.id,
      method: method?.id,
    });
    return;
  }
  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    logger.warn('Stake slashing aborted because customer not found', {
      subscription: subscription.id,
      invoice: invoice.id,
      customer: subscription.customer_id,
    });
    return;
  }
  const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
  if (!paymentIntent) {
    logger.warn('Stake slashing aborted because payment intent not found', {
      subscription: subscription.id,
      invoice: invoice.id,
    });
    return;
  }

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

  try {
    await lock.acquire();
    logger.debug('Acquired lock for stake slashing', {
      subscription: subscription.id,
      paymentIntent: paymentIntent.id,
    });

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

    // check the staking
    const client = method.getOcapClient();
    const address = await getSubscriptionStakeAddress(subscription, customer.did, method);
    const { state } = await client.getStakeState({ address });
    if (!state || !state.data?.value) {
      logger.warn('Stake slashing aborted because no staking state', {
        subscription: subscription.id,
        address,
      });
      return;
    }
    // check for staking for this subscription
    const data = JSON.parse(state.data.value || '{}');
    if (!data[subscription.id] && !state.nonce) {
      logger.warn('Stake slashing aborted because no staking for subscription', {
        subscription: subscription.id,
        address,
        data,
      });
      return;
    }

    // check for staking for amount
    const stakeEnough = await checkRemainingStake(method, currency, address, invoice.amount_remaining);
    if (!stakeEnough.enough) {
      logger.warn('Stake slashing aborted because no enough staking', {
        subscription: subscription.id,
        address,
        staked: stakeEnough.staked,
        revoked: stakeEnough.revoked,
      });
      return;
    }

    if (invoice.amount_remaining === '0') {
      logger.warn('Stake slashing aborted because amount_remaining is 0', {
        subscription: subscription.id,
        address,
        invoice: invoice.id,
      });
      return;
    }

    // do the slash
    const signed = await client.signSlashStakeTx({
      tx: {
        itx: {
          address,
          outputs: [
            { owner: wallet.address, tokens: [{ address: currency.contract, value: invoice.amount_remaining }] },
          ],
          message: 'uncollectible_past_due_invoice',
          data: {
            typeUrl: 'json',
            // @ts-ignore
            value: {
              appId: wallet.address,
              reason: '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: invoice.amount_remaining,
      address,
      txHash,
      invoice: invoice.id,
    });

    await paymentIntent.update({
      status: 'succeeded',
      amount_received: invoice.amount_remaining,
      capture_method: 'manual',
      last_payment_error: null,
      payment_details: {
        arcblock: {
          tx_hash: txHash,
          payer: getSubscriptionPaymentAddress(subscription, 'arcblock'),
          type: 'slash',
        },
      },
    });
    logger.info('PaymentIntent updated after stake slash', {
      subscription: subscription.id,
      paymentIntent: paymentIntent.id,
      status: 'succeeded',
    });
    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 updated after stake slash', {
      subscription: subscription.id,
      invoice: invoice.id,
      status: 'paid',
    });
  } finally {
    // Always release the lock
    await lock.release();
    logger.debug('Released lock for stake slashing', {
      subscription: subscription.id,
      paymentIntent: paymentIntent.id,
    });
  }
};

const ensureReturnStake = async (subscription: Subscription, paymentCurrencyId?: string, stakingAddress?: string) => {
  const paymentCurrency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
  if (!paymentCurrency) {
    logger.warn('Stake return skipped because no payment currency', {
      subscription: subscription.id,
      currency: subscription.currency_id,
    });
    return;
  }
  const paymentMethod = await PaymentMethod.findByPk(
    paymentCurrency.payment_method_id || subscription.default_payment_method_id
  );
  if (paymentMethod?.type !== 'arcblock') {
    logger.warn('Stake return skipped because payment method not arcblock', {
      subscription: subscription.id,
      paymentMethod: paymentMethod?.id,
    });
    return;
  }
  const address = stakingAddress || subscription?.payment_details?.arcblock?.staking?.address;
  if (!address) {
    logger.warn('Stake return skipped because no staking address', {
      subscription: subscription.id,
      address,
    });
    return;
  }

  // Handle any unpaid final invoice before stake return
  await handleFinalInvoicePayment(subscription, paymentMethod, paymentCurrency);

  const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod, paymentCurrencyId);

  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, result.return_amount);
  if (!stakeEnough.enough) {
    logger.warn('Stake return skipped because no enough staking', {
      subscription: subscription.id,
      address,
      staked: stakeEnough.staked,
      revoked: stakeEnough.revoked,
    });
    return;
  }
  if (result.return_amount !== '0') {
    const invoice = await Invoice.findOne({
      where: {
        billing_reason: 'stake',
        subscription_id: subscription.id,
        currency_id: paymentCurrency.id,
        status: 'paid',
      },
      order: [['created_at', 'DESC']],
    });
    // do the stake return
    const item = await Refund.create({
      type: 'stake_return',
      livemode: subscription.livemode,
      amount: result.return_amount,
      description: 'stake_return_for_subscription',
      status: 'pending',
      reason: 'requested_by_admin',
      currency_id: paymentCurrency.id,
      invoice_id: invoice?.id,
      customer_id: subscription.customer_id,
      payment_method_id: paymentMethod.id,
      payment_intent_id: invoice?.payment_intent_id || '',
      subscription_id: subscription.id,
      attempt_count: 0,
      attempted: false,
      next_attempt: 0,
      last_attempt_error: null,
      starting_balance: '0',
      ending_balance: '0',
      starting_token_balance: {},
      ending_token_balance: {},
      payment_details: {
        // @ts-ignore
        arcblock: {
          receiver: result.sender,
          staking: {
            address,
            tx_hash: '',
          },
        },
      },
    });
    logger.info('Created stake return for canceled subscription', {
      subscription: subscription.id,
      return_amount: result.return_amount,
      item: item.toJSON(),
    });
  } else {
    logger.info('Skipped stake return for subscription', {
      subscription: subscription.id,
      return_amount: result.return_amount,
    });
  }
};

const slashStakeOnCancel = async (subscription: Subscription) => {
  const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
  if (paymentMethod?.type !== 'arcblock') {
    logger.warn('Stake slashing skipped because payment method not arcblock', {
      subscription: subscription.id,
      paymentMethod: paymentMethod?.id,
    });
    return;
  }
  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    logger.warn('Stake slashing skipped because customer not found', {
      subscription: subscription.id,
      customer: subscription.customer_id,
    });
    return;
  }
  const currency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!currency) {
    logger.warn('Stake slashing skipped because currency not found', {
      subscription: subscription.id,
      currency: subscription.currency_id,
    });
    return;
  }
  const address = await getSubscriptionStakeAddress(subscription, customer.did, paymentMethod);
  const result = await getSubscriptionStakeSlashSetup(subscription, address, paymentMethod);
  const stakeEnough = await checkRemainingStake(paymentMethod, currency, address, result.return_amount);
  if (!stakeEnough.enough) {
    logger.warn('Stake slashing aborted because no enough staking', {
      subscription: subscription.id,
      address,
      staked: stakeEnough.revoked,
      revoked: stakeEnough.revoked,
    });
    return;
  }

  if (result.return_amount === '0') {
    logger.warn('Stake slashing aborted because amount_remaining is 0', {
      subscription: subscription.id,
      address,
      amount_remaining: result.return_amount,
    });
    return;
  }

  // FIXME: handle exist one more invoices
  const invoice = await Invoice.findOne({
    where: {
      subscription_id: subscription.id,
      billing_reason: 'slash_stake',
    },
  });
  if (invoice) {
    invoiceQueue.push({
      id: invoice.id,
      job: { invoiceId: invoice.id, retryOnError: true },
    });
    logger.info('Invoice job scheduled for slash stake', { invoice: invoice.id, subscription: subscription.id });
  } else {
    const { invoice: newInvoice } = await ensureInvoiceAndItems({
      customer,
      currency,
      subscription,
      trialing: false,
      metered: false,
      lineItems: [],
      props: {
        livemode: subscription.livemode,
        description: 'Slash stake',
        statement_descriptor: result.lastInvoice?.statement_descriptor,
        period_start: subscription.canceled_at,
        period_end: subscription.current_period_end as number,

        auto_advance: true,
        status: 'open',
        billing_reason: 'slash_stake',
        currency_id: subscription.currency_id,

        total: result.return_amount,
        subtotal: result.return_amount,
        amount_paid: '0',
        amount_remaining: result.return_amount,
        amount_due: result.return_amount,

        payment_settings: subscription.payment_settings,
        default_payment_method_id: subscription.default_payment_method_id,
        subscription_id: subscription.id,
      } as unknown as Invoice,
    });
    logger.info('New invoice created for stake slash', {
      subscription: subscription.id,
      invoice: newInvoice.id,
      amount: result.return_amount,
    });
    invoiceQueue.push({
      id: newInvoice.id,
      job: { invoiceId: newInvoice.id, retryOnError: true },
    });
    logger.info('Invoice job scheduled for slash stake', { invoice: newInvoice.id, subscription: subscription.id });
  }
};

const ensureRefundOnCancel = async (subscription: Subscription) => {
  // @ts-ignore
  const { refund } = subscription.cancelation_details;
  if (!['last', 'proration'].includes(refund)) {
    logger.warn('Refund skipped because refund type is not last or proration', {
      subscription: subscription.id,
      refund,
    });
    return;
  }
  const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
  if (!lastInvoice) {
    logger.warn('Refund skipped because no latest invoice', {
      subscription: subscription.id,
    });
    return;
  }

  const result = await getSubscriptionRefundSetup(subscription, subscription.cancel_at, lastInvoice.currency_id);
  if (result.remainingUnused === '0') {
    logger.warn('Refund skipped because unused amount is 0', {
      subscription: subscription.id,
      unused: result.unused,
    });
    return;
  }
  const currency = await PaymentCurrency.findByPk(lastInvoice?.currency_id);
  const item = await Refund.create({
    type: 'refund',
    livemode: subscription.livemode,
    amount: refund === 'last' ? result.remaining : result.remainingUnused,
    description: 'refund_transfer_on_subscription_cancel',
    status: 'pending',
    reason: 'requested_by_admin',
    currency_id: result.lastInvoice?.currency_id,
    customer_id: subscription.customer_id,
    payment_method_id: currency?.payment_method_id || subscription.default_payment_method_id,
    payment_intent_id: result.lastInvoice.payment_intent_id as string,
    invoice_id: result.lastInvoice.id,
    subscription_id: subscription.id,
    attempt_count: 0,
    attempted: false,
    next_attempt: 0,
    last_attempt_error: null,
    starting_balance: '0',
    ending_balance: '0',
    starting_token_balance: {},
    ending_token_balance: {},
    metadata: {
      requested_by: subscription.cancelation_details?.requested_by,
      unused_period_start: refund === 'last' ? subscription.current_period_start : subscription.cancel_at,
      unused_period_end: subscription.current_period_end,
    },
  });
  logger.info('Created refund for canceled subscription', {
    subscription: subscription.id,
    refund: item.toJSON(),
    refundType: refund,
  });
};

const ensureReturnOverdraftProtectionStake = async (subscription: Subscription, paymentCurrencyId?: string) => {
  const { enabled, remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(
    subscription,
    paymentCurrencyId
  );
  if (!enabled && remaining === '0' && revokedStake === '0') {
    logger.info('Return overdraft protection stake skipped because no remaining', {
      subscription: subscription.id,
      remaining,
      revokedStake,
    });
    return;
  }
  const paymentCurrency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
  if (!paymentCurrency) {
    logger.warn('Return overdraft protection stake skipped because currency not found', {
      subscription: subscription.id,
      currency: subscription.currency_id,
    });
    return;
  }
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    logger.warn('Return overdraft protection stake skipped because payment method not found', {
      subscription: subscription.id,
      paymentMethod: paymentCurrency.payment_method_id,
    });
    return;
  }
  if (paymentMethod?.type !== 'arcblock') {
    logger.info('Return overdraft protection stake skipped because payment method is not arcblock', {
      subscription: subscription.id,
      paymentMethod: paymentMethod?.type,
    });
    return;
  }
  await returnOverdraftProtectionStake(subscription, paymentCurrencyId);
  logger.info('Overdraft protection stake returned', { subscription: subscription.id });
  if (subscription.overdraft_protection?.enabled) {
    await subscription.update({
      // @ts-ignore
      overdraft_protection: {
        ...(subscription.overdraft_protection || {}),
        enabled: false,
      },
    });
    logger.info('Overdraft protection disabled', { subscription: subscription.id });
  }
};

const ensureSlashOverdraftProtectionStake = async (subscription: Subscription) => {
  const { remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(subscription);
  if (remaining === '0' && revokedStake === '0') {
    logger.info('Slash overdraft protection stake skipped because no remaining', {
      subscription: subscription.id,
      remaining,
      revokedStake,
    });
    return;
  }
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    logger.warn('Slash overdraft protection stake skipped because currency not found', {
      subscription: subscription.id,
      currency: subscription.currency_id,
    });
    return;
  }
  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    logger.warn('Slash overdraft protection stake skipped because payment method not found', {
      subscription: subscription.id,
      paymentMethod: paymentCurrency.payment_method_id,
    });
    return;
  }
  if (paymentMethod?.type !== 'arcblock') {
    logger.info('Slash overdraft protection stake skipped because payment method is not arcblock', {
      subscription: subscription.id,
      paymentMethod: paymentMethod?.type,
    });
    return;
  }
  await slashOverdraftProtectionStake(subscription);
  logger.info('Overdraft protection stake slashed', { subscription: subscription.id });
  if (subscription.overdraft_protection?.enabled) {
    await subscription.update({
      // @ts-ignore
      overdraft_protection: {
        ...(subscription.overdraft_protection || {}),
        enabled: false,
      },
    });
  }
};

// generate invoice for subscription periodically
export const handleSubscription = async (job: SubscriptionJob) => {
  logger.info('handle subscription', job);

  const subscription = await Subscription.findByPk(job.subscriptionId);
  if (!subscription) {
    logger.warn('Subscription not found', { subscription: job.subscriptionId });
    return;
  }
  if (EXPECTED_SUBSCRIPTION_STATUS.includes(subscription.status) === false) {
    logger.warn('Subscription status not expected', { subscription: subscription.id, status: subscription.status });
    return;
  }
  const supportAutoCharge = await PaymentMethod.supportAutoCharge(subscription.default_payment_method_id);
  if (supportAutoCharge === false) {
    logger.warn('Subscription does not support auto charge', { subscription: subscription.id });
    return;
  }

  const now = dayjs().unix();

  // Do we need to cancel the subscription
  if (subscription.isImmutable() === false) {
    if (shouldCancelSubscription(subscription)) {
      const previousStatus = subscription.status;
      await handleSubscriptionBeforeCancel(subscription);
      await subscription.update({ status: 'canceled', canceled_at: now });
      logger.info('Subscription canceled on period end or schedule', {
        subscription: subscription.id,
        previousStatus,
      });

      if (previousStatus === 'past_due' && job.action === 'cancel') {
        // slash overdraft protection stake
        await slashOverdraftProtectionQueue.pushAndWait({
          id: `slash-overdraft-protection-${subscription.id}`,
          job: { subscriptionId: subscription.id },
        });
        logger.info('Overdraft protection stake slash job scheduled', { subscription: subscription.id });
        await handleStakeSlashAfterCancel(subscription);
      }
      return;
    }
  }

  // Do we need to resume the subscription
  if (subscription.pause_collection?.resumes_at && subscription.pause_collection?.resumes_at <= now) {
    await subscription.update({ status: 'active', pause_collection: undefined });
    logger.warn(`Subscription resumed as scheduled: ${job.subscriptionId}`);
  }

  // can we create new invoice for this subscription?
  if (subscription.status === 'trialing' && subscription.trial_end && subscription.trial_end > now) {
    logger.warn(`Subscription trialing period not ended: ${job.subscriptionId}`);
    await addSubscriptionJob(subscription, 'cycle', false, subscription.trial_end);
    return;
  }
  if (subscription.status === 'active' && subscription.current_period_end > now) {
    logger.warn(`Subscription current period not ended: ${job.subscriptionId}`);
    await addSubscriptionJob(subscription, 'cycle');
    return;
  }

  if (subscription.isActive() === false) {
    logger.warn(`Subscription not active: ${job.subscriptionId}, so new invoice is skipped`);
    return;
  }

  await handleSubscriptionWhenActive(subscription);
};

export const subscriptionQueue = createQueue<SubscriptionJob>({
  name: 'subscription',
  onJob: handleSubscription,
  options: {
    concurrency: 5,
    maxRetries: 3,
    enableScheduledJob: true,
  },
});

/**
 * Handle credit subscription recovery after system restart
 * Checks for subscriptions that may have missed billing periods during downtime
 */
export const handleCreditSubscriptionRecovery = async () => {
  const lock = getLock('creditSubscriptionRecovery');
  if (lock.locked) {
    return;
  }

  logger.info('Starting credit subscription recovery check');
  try {
    await lock.acquire();

    // Find active credit subscriptions that might need recovery
    const creditSubscriptions = await Subscription.findAll({
      where: {
        status: ['active', 'trialing'],
      },
      include: [
        {
          model: PaymentCurrency,
          as: 'paymentCurrency',
          where: {
            type: 'credit',
          },
        },
      ],
    });

    const now = dayjs().unix();
    const recoveredSubscriptions: string[] = [];

    const results = await Promise.allSettled(
      creditSubscriptions.map(async (subscription) => {
        // Check if subscription period has ended
        if (subscription.current_period_end && now > subscription.current_period_end) {
          const previousPeriodStart = subscription.current_period_start;
          const previousPeriodEnd =
            subscription.status === 'trialing' ? subscription.trial_end : subscription.current_period_end;

          const setup = getSubscriptionCycleSetup(
            subscription.pending_invoice_item_interval,
            previousPeriodEnd as number,
            { catchUp: true, maxMissedPeriods: 100 }
          );

          if (setup.missedPeriods > 0) {
            logger.info('Credit subscription requires recovery', {
              subscription: subscription.id,
              missedPeriods: setup.missedPeriods,
              originalPeriodEnd: setup.recovery?.originalPeriodEnd,
              newPeriodStart: setup.period.start,
              newPeriodEnd: setup.period.end,
            });

            // Update subscription to current period
            await subscription.update({
              current_period_start: setup.period.start,
              current_period_end: setup.period.end,
            });
            if (subscription.credit_schedule_state && previousPeriodStart !== setup.period.start) {
              await resetPeriodGrantCounter(subscription);
            }

            // Schedule next billing cycle
            await addSubscriptionJob(subscription, 'cycle', true, setup.period.end);

            // Create audit event
            await createEvent('Subscription', 'system.recovery', subscription, {
              missedPeriods: setup.missedPeriods,
              recovery: setup.recovery,
              recoveryType: 'startup',
            });

            recoveredSubscriptions.push(subscription.id);
          }
        }
      })
    );

    const failed = results.filter((r) => r.status === 'rejected').length;
    if (failed > 0) {
      logger.warn(`Failed to process ${failed} credit subscriptions in recovery`);
    }

    if (recoveredSubscriptions.length > 0) {
      logger.info('Credit subscription recovery completed', {
        recoveredCount: recoveredSubscriptions.length,
        subscriptions: recoveredSubscriptions,
      });
    } else {
      logger.info('No credit subscriptions required recovery');
    }
  } catch (error) {
    logger.error('Error in credit subscription recovery:', error);
  } finally {
    lock.release();
  }
};

export const startSubscriptionQueue = async () => {
  const lock = getLock('startSubscriptionQueue');
  if (lock.locked) {
    return;
  }
  logger.info('startSubscriptionQueue');
  try {
    await lock.acquire();

    // First handle credit subscription recovery
    await handleCreditSubscriptionRecovery();

    const subscriptions = await Subscription.findAll({
      where: {
        status: EXPECTED_SUBSCRIPTION_STATUS,
      },
    });

    const results = await Promise.allSettled(
      subscriptions.map(async (x) => {
        const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.default_payment_method_id);
        if (supportAutoCharge === false) {
          return;
        }
        if (['past_due', 'paused'].includes(x.status)) {
          const willCancel = x.cancel_at || x.cancel_at_period_end;
          if (x.status === 'past_due' && willCancel) {
            const existingJob = await subscriptionQueue.get(`cancel-${x.id}`);
            if (!existingJob) {
              await addSubscriptionJob(x, 'cancel', true, x.cancel_at || x.current_period_end);
            }
          }
          logger.info(`skip add cycle subscription job because status is ${x.status}`, {
            subscription: x.id,
            action: 'cycle',
          });
          return;
        }
        logger.info('add subscription job', { subscription: x.id, action: 'cycle' });
        await addSubscriptionJob(x, 'cycle', process.env.PAYMENT_RELOAD_SUBSCRIPTION_JOBS === '1');
      })
    );

    const failed = results.filter((r) => r.status === 'rejected').length;
    if (failed > 0) {
      logger.warn(`Failed to process ${failed} subscriptions in startSubscriptionQueue`);
    }

    await batchHandleStripeSubscriptions();
  } catch (error) {
    logger.error('Error in startSubscriptionQueue:', error);
  } finally {
    lock.release();
  }
};

export const slashStakeQueue = createQueue({
  name: 'slashStake',
  onJob: async (job) => {
    const { subscriptionId } = job;
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      return;
    }
    await slashStakeOnCancel(subscription);
  },
  options: {
    concurrency: 1,
    maxRetries: 5,
    retryDelay: 1000,
    maxTimeout: 60000,
    enableScheduledJob: true,
  },
});

export const returnStakeQueue = createQueue({
  name: 'returnStake',
  onJob: async (job) => {
    const { subscriptionId, stakingAddress, paymentCurrencyId } = job;
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      return;
    }
    await ensureReturnStake(subscription, paymentCurrencyId, stakingAddress);
  },
  options: {
    concurrency: 1,
    maxRetries: 5,
    retryDelay: 1000,
    maxTimeout: 60000,
    enableScheduledJob: true,
  },
});

export const subscriptionCancelRefund = createQueue({
  name: 'subscription-cancel-refund',
  onJob: async (job) => {
    const { subscriptionId } = job;
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      return;
    }
    await ensureRefundOnCancel(subscription);
  },
  options: {
    concurrency: 1,
    maxRetries: 5,
    retryDelay: 1000,
    maxTimeout: 60000,
    enableScheduledJob: true,
  },
});

export const returnOverdraftProtectionQueue = createQueue({
  name: 'returnOverdraftProtection',
  onJob: async (job) => {
    const { subscriptionId, paymentCurrencyId } = job;
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      return;
    }
    await ensureReturnOverdraftProtectionStake(subscription, paymentCurrencyId);
  },
  options: {
    concurrency: 1,
    maxRetries: 5,
    retryDelay: 1000,
    maxTimeout: 60000,
    enableScheduledJob: true,
  },
});

export const slashOverdraftProtectionQueue = createQueue({
  name: 'slashOverdraftProtection',
  onJob: async (job) => {
    const { subscriptionId } = job;
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) {
      return;
    }
    await ensureSlashOverdraftProtectionStake(subscription);
  },
  options: {
    concurrency: 1,
    maxRetries: 5,
    retryDelay: 1000,
    maxTimeout: 60000,
    enableScheduledJob: true,
  },
});

export async function addSubscriptionJob(
  subscription: Subscription,
  action: 'cycle' | 'cancel' | 'resume',
  replace?: boolean,
  runAt?: number,
  sync?: boolean
) {
  const fn = sync ? 'pushAndWait' : 'push';
  const cycleJobId = subscription.id;
  const jobId = action === 'cycle' ? cycleJobId : `${action}-${subscription.id}`;
  const cycleJob = await subscriptionQueue.get(cycleJobId);
  if (replace && cycleJob) {
    await subscriptionQueue.delete(cycleJobId);
    logger.info(`subscription cycle job replaced with ${action} job`, { subscription: subscription.id });
  }
  if (action === 'cycle') {
    if (!cycleJob || (replace && cycleJob)) {
      const renewalTime = runAt || subscription.current_period_end;
      await subscriptionQueue[fn]({
        id: jobId,
        job: { subscriptionId: subscription.id, action },
        runAt: renewalTime,
      });

      // Schedule slippage pre-check 1 hour before renewal for dynamic pricing subscriptions
      try {
        await scheduleSlippagePreCheck(subscription, renewalTime);
      } catch (error) {
        // Log but don't fail the main job scheduling
        logger.error('Failed to schedule slippage pre-check', {
          subscriptionId: subscription.id,
          renewalTime,
          error,
        });
      }
    }
  } else {
    await subscriptionQueue.delete(jobId);
    await subscriptionQueue[fn]({
      id: jobId,
      job: { subscriptionId: subscription.id, action },
      runAt: runAt || subscription.current_period_end,
    });

    // Cancel slippage pre-check when subscription is being cancelled
    if (action === 'cancel') {
      try {
        await cancelSlippagePreCheck(subscription.id);
      } catch (error) {
        logger.error('Failed to cancel slippage pre-check', {
          subscriptionId: subscription.id,
          error,
        });
      }
    }
  }
}

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

events.on('customer.subscription.past_due', async (subscription: Subscription) => {
  await subscriptionQueue.delete(subscription.id);
  await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
  await addSubscriptionJob(subscription, 'cancel', true, subscription.cancel_at || subscription.current_period_end);
  logger.info('subscription cancel job scheduled after past_due', {
    subscription: subscription.id,
    runAt: subscription.cancel_at || subscription.current_period_end,
  });
});

events.on('customer.subscription.recovered', async (subscription: Subscription) => {
  logger.info('subscription cancel job replaced after recover', { subscription: subscription.id });
  await subscriptionQueue.delete(`cancel-${subscription.id}`);
  await subscriptionQueue.delete(subscription.id);
  const doc = await Subscription.findByPk(subscription.id);
  await handleSubscriptionAfterRecover(doc!);
});

events.on('customer.subscription.deleted', async (subscription: Subscription) => {
  ensurePassportRevoked(subscription).catch((err) => {
    logger.error('ensurePassportRevoked failed', { error: err, subscription: subscription.id });
  });
  // FIXME: ensure invoices that are open or uncollectible are voided
  if (
    subscription.cancelation_details?.refund &&
    ['last', 'proration'].includes(subscription.cancelation_details.refund)
  ) {
    subscriptionCancelRefund.push({
      id: `subscription-cancel-refund-${subscription.id}`,
      job: { subscriptionId: subscription.id },
    });
  }
  const { remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(subscription);
  if (remaining !== '0' || revokedStake !== '0') {
    // need return overdraft protection stake
    await returnOverdraftProtectionQueue.pushAndWait({
      id: `return-overdraft-protection-${subscription.id}`,
      job: { subscriptionId: subscription.id },
    });
    logger.info('Overdraft protection stake return job scheduled', { subscription: subscription.id });
  }
  if (subscription.cancelation_details?.slash_stake) {
    slashStakeQueue.push({
      id: `slash-stake-${subscription.id}`,
      job: { subscriptionId: subscription.id },
    });
  } else if (subscription.cancelation_details?.return_stake) {
    returnStakeQueue.push({ id: `return-stake-${subscription.id}`, job: { subscriptionId: subscription.id } });
  }
});

events.on('customer.subscription.upgraded', async (subscription: Subscription) => {
  await addSubscriptionJob(subscription, 'cycle', true, subscription.current_period_end);
  logger.info('subscription cycle job rescheduled after upgrade', {
    subscription: subscription.id,
    runAt: subscription.current_period_end,
  });
});

events.on('customer.stake.revoked', async ({ subscriptionId, tx }: { subscriptionId: string; tx: any }) => {
  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    return;
  }

  const { address } = tx.tx.itxJson;
  if (address === subscription.overdraft_protection?.payment_details?.arcblock?.staking?.address) {
    // revoke overdraft protection stake
    await subscription.update({
      // @ts-ignore
      overdraft_protection: {
        ...(subscription.overdraft_protection || {}),
        enabled: false,
      },
    });
    slashOverdraftProtectionQueue.push({
      id: `slash-overdraft-protection-${subscription.id}`,
      job: { subscriptionId: subscription.id },
    });
    logger.info('Overdraft protection stake slash job scheduled', { subscription: subscription.id });
    return;
  }

  if (subscription.isActive() === false) {
    return;
  }

  logger.info('subscription scheduled to cancel after stake revoked', { subscription: subscriptionId, tx: tx.hash });
  await subscription.update({
    cancel_at_period_end: true,
    cancel_at: subscription.current_period_end,
    cancelation_details: {
      // FIXME: 这里理论上需要通知用户
      reason: 'stake_revoked',
      feedback: 'other',
      comment: `Revoked by ${tx.tx.from} with tx ${tx.hash}`,
    },
    // 关闭订阅守护
    // @ts-ignore
    overdraft_protection: {
      ...(subscription.overdraft_protection || {}),
      enabled: false,
    },
  });
  slashOverdraftProtectionQueue.push({
    id: `slash-overdraft-protection-${subscription.id}`,
    job: { subscriptionId: subscription.id },
  });
  logger.info('Overdraft protection stake slash job scheduled', { subscription: subscription.id });
  await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
  await addSubscriptionJob(subscription, 'cancel', true, subscription.current_period_end);
});

events.on('setup_intent.succeeded', async (setupIntent: SetupIntent) => {
  logger.info('setup intent succeeded', { setupIntent: setupIntent.id });
  if (setupIntent.metadata?.from_currency && setupIntent?.metadata?.subscription_id) {
    const subscription = await Subscription.findByPk(setupIntent.metadata.subscription_id);
    if (!subscription) {
      logger.info('skip return stake because no subscription found', { setupIntent: setupIntent.id });
      return;
    }
    const stakingInvoice = await Invoice.findOne({
      where: {
        subscription_id: subscription.id,
        billing_reason: 'stake',
        currency_id: setupIntent.metadata?.from_currency,
        status: 'paid',
      },
      order: [['created_at', 'DESC']],
    });
    logger.info('staking invoice', { stakingInvoice });
    if (stakingInvoice) {
      returnStakeQueue.push({
        id: `return-stake-${subscription.id}-${stakingInvoice.id}`,
        job: {
          subscriptionId: subscription.id,
          stakingAddress: stakingInvoice?.metadata?.payment_details?.arcblock?.address,
          paymentCurrencyId: setupIntent.metadata?.from_currency,
        },
      });
      logger.info('subscription return stake job scheduled', {
        jobId: `return-stake-${subscription.id}-${stakingInvoice.id}`,
        subscription: subscription.id,
        stakingAddress: stakingInvoice?.metadata?.payment_details?.arcblock?.address,
        paymentCurrencyId: setupIntent.metadata?.from_currency,
      });
    }
    const { remaining, revokedStake } = await isSubscriptionOverdraftProtectionEnabled(
      subscription,
      setupIntent.metadata?.from_currency
    );

    if (remaining !== '0' || revokedStake !== '0') {
      try {
        returnOverdraftProtectionQueue.push({
          id: `return-overdraft-protection-${subscription.id}`,
          job: { subscriptionId: subscription.id, paymentCurrencyId: setupIntent.metadata?.from_currency },
        });
        logger.info('Overdraft protection stake return job scheduled', {
          subscription: subscription.id,
          paymentCurrencyId: setupIntent.metadata?.from_currency,
        });
      } catch (error) {
        logger.error('create return overdraft protection stake job failed', { error, subscription: subscription.id });
      }
    }

    try {
      const migrationResult = await migrateSubscriptionPaymentMethodInvoice(
        subscription,
        setupIntent.metadata?.from_currency,
        setupIntent.metadata?.to_currency
      );
      if (migrationResult.migrated) {
        logger.info('Subscription payment method billing migration completed', {
          subscription: subscription.id,
          migrated: migrationResult.migrated,
          oldInvoice: migrationResult.oldInvoice?.id,
          newInvoice: migrationResult.newInvoice?.id,
        });
      }
    } catch (error) {
      logger.error('Failed to migrate billing for payment method change', {
        subscription: subscription.id,
        fromCurrency: setupIntent.metadata?.from_currency,
        toCurrency: setupIntent.metadata?.to_currency,
        error,
      });
    }
  }
});

// ============= Slippage Pre-Check Job =============
// Checks slippage 1 hour before subscription renewal and sends warning notification if below threshold

type SlippagePreCheckJob = {
  subscriptionId: string;
  renewalTime: number;
};

/**
 * Handle slippage pre-check: Check if current exchange rate is below min acceptable rate
 * before subscription renewal and send warning notification
 */
async function handleSlippagePreCheck(job: SlippagePreCheckJob): Promise<void> {
  const { subscriptionId, renewalTime } = job;

  const subscription = await Subscription.findByPk(subscriptionId);
  if (!subscription) {
    logger.warn('Slippage pre-check: Subscription not found', { subscriptionId });
    return;
  }

  // Skip if subscription is not in active status
  if (!['active', 'trialing'].includes(subscription.status)) {
    logger.info('Slippage pre-check: Skipping non-active subscription', {
      subscriptionId,
      status: subscription.status,
    });
    return;
  }

  // Skip if no slippage config - having slippage_config means it's a dynamic pricing subscription
  const slippageConfig = subscription.slippage_config;
  if (!slippageConfig || !slippageConfig.min_acceptable_rate) {
    logger.info('Slippage pre-check: No slippage config or min_acceptable_rate', { subscriptionId });
    return;
  }

  // Get payment currency and payment method
  const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
  if (!paymentCurrency) {
    logger.info('Slippage pre-check: Payment currency not found', { subscriptionId });
    return;
  }

  const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
  if (!paymentMethod) {
    logger.info('Slippage pre-check: Payment method not found', { subscriptionId });
    return;
  }

  // Get current exchange rate
  try {
    const exchangeRateService = getExchangeRateService();
    const rateSymbol = getExchangeRateSymbol(paymentCurrency.symbol, paymentMethod.type as any);

    if (!rateSymbol) {
      logger.info('Slippage pre-check: No exchange rate symbol for currency', {
        subscriptionId,
        currencyId: paymentCurrency.id,
        symbol: paymentCurrency.symbol,
      });
      return;
    }

    const rateResult = await exchangeRateService.getRate(rateSymbol);
    const currentRate = rateResult?.rate;

    if (!currentRate) {
      logger.warn('Slippage pre-check: Failed to get current exchange rate', {
        subscriptionId,
        rateSymbol,
      });
      return;
    }

    const belowThreshold = isRateBelowMinAcceptableRate(String(currentRate), slippageConfig.min_acceptable_rate);

    if (belowThreshold) {
      // Emit warning notification event
      events.emit('subscription.slippage_warning', subscription, {
        currentRate: String(currentRate),
        minAcceptableRate: slippageConfig.min_acceptable_rate,
        renewalTime,
      });

      logger.info('Slippage pre-check: Warning sent due to rate below threshold', {
        subscriptionId,
        currentRate,
        minAcceptableRate: slippageConfig.min_acceptable_rate,
        renewalTime,
      });
    } else {
      logger.info('Slippage pre-check: Rate is acceptable', {
        subscriptionId,
        currentRate,
        minAcceptableRate: slippageConfig.min_acceptable_rate,
      });
    }
  } catch (error) {
    logger.error('Slippage pre-check: Failed to check exchange rate', {
      subscriptionId,
      error,
    });
  }
}

export const slippagePreCheckQueue = createQueue<SlippagePreCheckJob>({
  name: 'slippage_pre_check',
  onJob: handleSlippagePreCheck,
  options: {
    concurrency: 10,
    maxRetries: 3,
    retryDelay: 60000, // 1 minute
    maxTimeout: 60000,
    enableScheduledJob: true,
  },
});

/**
 * Schedule slippage pre-check job 1 hour before subscription renewal
 * Should be called when scheduling the cycle job
 */
export async function scheduleSlippagePreCheck(subscription: Subscription, renewalTime: number): Promise<void> {
  // Only schedule for subscriptions with slippage config and min_acceptable_rate
  // Having slippage_config means it's a dynamic pricing subscription
  const slippageConfig = subscription.slippage_config;
  if (!slippageConfig || !slippageConfig.min_acceptable_rate) {
    return;
  }

  // Schedule pre-check 1 hour before renewal
  const preCheckTime = renewalTime - 3600; // 1 hour before
  const now = Math.floor(Date.now() / 1000);

  // Only schedule if pre-check time is in the future
  if (preCheckTime <= now) {
    logger.info('Slippage pre-check: Pre-check time is in the past, skipping', {
      subscriptionId: subscription.id,
      renewalTime,
      preCheckTime,
      now,
    });
    return;
  }

  const jobId = `slippage-pre-check-${subscription.id}-${renewalTime}`;

  // Delete existing job if any
  await slippagePreCheckQueue.delete(jobId);

  await slippagePreCheckQueue.push({
    id: jobId,
    job: {
      subscriptionId: subscription.id,
      renewalTime,
    },
    runAt: preCheckTime,
  });

  logger.info('Slippage pre-check: Job scheduled', {
    subscriptionId: subscription.id,
    renewalTime,
    preCheckTime,
    jobId,
  });
}

/**
 * Cancel slippage pre-check job for a subscription
 */
export async function cancelSlippagePreCheck(subscriptionId: string, renewalTime?: number): Promise<void> {
  if (renewalTime) {
    const jobId = `slippage-pre-check-${subscriptionId}-${renewalTime}`;
    await slippagePreCheckQueue.delete(jobId);
    logger.info('Slippage pre-check: Specific job cancelled', { subscriptionId, renewalTime, jobId });
  } else {
    // Delete all pre-check jobs for this subscription
    // Note: This is a fallback that searches through the queue
    const jobs = await slippagePreCheckQueue.store.findJobs({});
    // eslint-disable-next-line no-await-in-loop
    for (const job of jobs) {
      if (job.job?.subscriptionId === subscriptionId) {
        // eslint-disable-next-line no-await-in-loop
        await slippagePreCheckQueue.delete(job.id);
        logger.info('Slippage pre-check: Job cancelled', { subscriptionId, jobId: job.id });
      }
    }
  }
}
