/* eslint-disable no-continue */
/* eslint-disable no-await-in-loop */
import { env } from '@blocklet/sdk/lib/env';
import merge from 'lodash/merge';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { Op } from 'sequelize';

import logger from '../../libs/logger';
import { getSubscriptionItemPrice } from '../../libs/subscription';
import { sleep } from '../../libs/util';
import {
  AutoRechargeConfig,
  CheckoutSession,
  Customer,
  Discount,
  Coupon,
  Invoice,
  InvoiceItem,
  PaymentCurrency,
  PaymentIntent,
  PaymentMethod,
  Price,
  Product,
  Subscription,
  SubscriptionItem,
  TLineItemExpanded,
} from '../../store/models';
import { syncStripeInvoice } from './handlers/invoice';
// eslint-disable-next-line import/no-cycle
import { syncStripePayment } from './handlers/payment-intent';
import { getLock } from '../../libs/lock';
import { getPriceUintAmountByCurrency } from '../../libs/price';

export async function ensureStripeProduct(internal: Product, method: PaymentMethod) {
  const client = method.getStripeClient();
  const result = await client.products.search({ query: `metadata['id']:'${internal.id}'` });
  if (result.data.length > 0) {
    return result.data[0] as any;
  }

  const attrs: any = {
    name: internal.name,
    description: internal.description,
    metadata: {
      appPid: env.appPid,
      id: internal.id,
    },
  };
  if (internal.unit_label) {
    attrs.unit_label = internal.unit_label;
  }
  // if (internal.statement_descriptor) {
  //   attrs.statement_descriptor = '';
  // }

  const product = await client.products.create(attrs);
  await internal.update({ metadata: merge(internal.metadata || {}, { stripe_id: product.id }) });
  logger.info('product created on stripe', { local: internal.id, remote: product.id });

  return product;
}

export async function ensureStripePrice(internal: Price, method: PaymentMethod, currency: PaymentCurrency) {
  const client = method.getStripeClient();
  const result = await client.prices.search({ query: `metadata['id']:'${internal.id}'` });
  if (result.data.length > 0) {
    return result.data[0] as any;
  }

  // create stripe product
  const local = await Product.findByPk(internal.product_id);
  const product = await ensureStripeProduct(local as Product, method);

  const attrs: any = {
    product: product.id,
    currency: currency.symbol.toLowerCase(),
    nickname: internal.nickname as string,
    lookup_key: internal.lookup_key as string,
    billing_scheme: internal.billing_scheme as any,
    metadata: {
      appPid: env.appPid,
      id: internal.id,
    },
  };

  if (internal.type === 'recurring') {
    attrs.recurring = {
      interval: internal.recurring?.interval,
      interval_count: internal.recurring?.interval_count,
    };
    if (internal.recurring?.usage_type) {
      attrs.recurring.usage_type = internal.recurring.usage_type;
    }
    if (internal.recurring?.usage_type === 'metered') {
      attrs.recurring.aggregate_usage = internal.recurring.aggregate_usage;
    }
  }
  if (internal.tiers_mode) {
    attrs.tiers_mode = internal.tiers_mode;
  }
  if (internal.tiers_mode === 'graduated' || internal.tiers_mode === 'volume') {
    attrs.tiers = internal.tiers;
  } else if (internal.transform_quantity) {
    attrs.transform_quantity = internal.transform_quantity;
  }
  if (internal.custom_unit_amount) {
    attrs.custom_unit_amount = internal.custom_unit_amount;
  } else {
    attrs.unit_amount = Number(getPriceUintAmountByCurrency(internal, currency.id));
  }

  // create stripe price
  const price = await client.prices.create(attrs);
  await internal.update({ metadata: merge(internal.metadata || {}, { stripe_id: price.id }) });
  logger.info('price created on stripe', { local: internal.id, remote: price.id });

  // update product default price
  if (!product.default_price) {
    await client.products.update(product.id, { default_price: price.id });
  }

  return price;
}

export async function ensureStripeCustomer(internal: Customer, method: PaymentMethod) {
  const client = method.getStripeClient();

  // 1. check local metadata
  if (internal.metadata?.stripe_id) {
    try {
      const customer = await client.customers.retrieve(internal.metadata.stripe_id);
      if (customer) {
        return customer;
      }
    } catch (error) {
      logger.warn('Stored Stripe customer ID not found, will recreate', {
        customerId: internal.id,
        stripeId: internal.metadata.stripe_id,
      });
    }
  }

  // 2. search customer on stripe
  const result = await client.customers.search({ query: `metadata['did']:'${internal.did}'` });
  if (result.data.length > 0) {
    const stripeCustomer = result.data[0];
    if (stripeCustomer) {
      // update local metadata
      if (!internal.metadata?.stripe_id || internal.metadata.stripe_id !== stripeCustomer!.id) {
        await internal.update({
          metadata: merge(internal.metadata || {}, {
            stripe_id: stripeCustomer!.id,
            stripe_invoice_prefix: stripeCustomer!.invoice_prefix,
          }),
        });
      }
      return stripeCustomer;
    }
  }

  // 3. create new customer, let stripe generate invoice prefix
  const customer = await client.customers.create({
    name: internal.name,
    email: internal.email,
    phone: internal.phone,
    metadata: {
      appPid: env.appPid,
      id: internal.id,
      did: internal.did,
    },
  });

  // 4. update local metadata
  await internal.update({
    metadata: merge(internal.metadata || {}, {
      stripe_id: customer.id,
      stripe_invoice_prefix: customer.invoice_prefix,
    }),
  });

  logger.info('customer created on stripe', { local: internal.id, remote: customer.id });

  return customer;
}

export async function ensureStripePaymentCustomer(internal: any, method: PaymentMethod) {
  const client = method.getStripeClient();
  const lock = getLock(`stripe-customer-${internal.customer_id}`);
  await lock.acquire();
  try {
    let customer = null;
    if (internal.payment_details?.stripe?.customer_id) {
      customer = await client.customers.retrieve(internal.payment_details.stripe.customer_id);
    } else {
      const local = await Customer.findByPk(internal.customer_id);
      customer = await ensureStripeCustomer(local as Customer, method);
    }
    return customer;
  } finally {
    lock.release();
  }
}

export async function ensureStripePaymentIntent(
  internal: PaymentIntent,
  method: PaymentMethod,
  currency: PaymentCurrency,
  checkoutSessionId?: string
) {
  const client = method.getStripeClient();

  let stripeIntent = null;
  if (internal.payment_details?.stripe?.payment_intent_id) {
    stripeIntent = await client.paymentIntents.retrieve(internal.payment_details.stripe.payment_intent_id);
    // Ensure Stripe metadata has local PI id + checkoutSessionId for webhook matching
    const meta = stripeIntent.metadata || {};
    if (meta.id !== internal.id || (checkoutSessionId && meta.checkoutSessionId !== checkoutSessionId)) {
      stripeIntent = await client.paymentIntents.update(stripeIntent.id, {
        metadata: { ...meta, appPid: env.appPid, id: internal.id, checkoutSessionId: checkoutSessionId || '' },
      });
    }
  } else {
    const customer = await ensureStripePaymentCustomer(internal, method);
    stripeIntent = await client.paymentIntents.create({
      amount: Number(internal.amount),
      currency: currency.symbol.toLowerCase(),
      customer: customer.id,
      automatic_payment_methods: {
        enabled: true,
        allow_redirects: 'never',
      },
      metadata: {
        appPid: env.appPid,
        id: internal.id,
        checkoutSessionId: checkoutSessionId || '',
      },
    });
    logger.info('stripe payment intent created', { local: internal.id, remote: stripeIntent.id });

    await internal.update({
      payment_details: {
        stripe: {
          customer_id: customer.id,
          payment_intent_id: stripeIntent.id,
        },
      },
    });
  }

  return stripeIntent;
}

export async function ensureStripeSetupIntentForCheckoutSession(
  checkoutSession: CheckoutSession,
  method: PaymentMethod,
  metadata: Record<string, string>
) {
  const client = method.getStripeClient();
  const customer = await ensureStripePaymentCustomer(checkoutSession, method);
  const setupIntent = await client.setupIntents.create({
    customer: customer.id,
    payment_method_types: ['card'],
    usage: 'off_session',
    metadata,
  });
  await checkoutSession.update({
    payment_details: {
      ...(checkoutSession.payment_details || {}),
      stripe: {
        ...(checkoutSession.payment_details?.stripe || {}),
        setup_intent_id: setupIntent.id,
      },
    },
  });
  return setupIntent;
}

export async function ensureStripeSubscription(
  internal: Subscription,
  method: PaymentMethod,
  currency: PaymentCurrency,
  items: TLineItemExpanded[],
  trialInDays: number = 0,
  trialEnd: number = 0
) {
  const client = method.getStripeClient();

  let stripeSubscription: any = null;
  if (internal.payment_details?.stripe?.subscription_id) {
    stripeSubscription = await client.subscriptions.retrieve(internal.payment_details.stripe.subscription_id, {
      expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
    });
    // Handle subscription discount updates
    await updateSubscriptionDiscounts(client, stripeSubscription, internal, method, currency);
  } else {
    const customer = await ensureStripePaymentCustomer(internal, method);
    const prices = await Promise.all(
      items.map(async (x: any) => {
        const price = x.upsell_price || x.price;
        x.stripePrice = await ensureStripePrice(price, method, currency);
        return x;
      })
    );

    const recurringItems = prices
      .filter((x) => x.price.type === 'recurring')
      .map((x) => {
        const price = x.upsell_price || x.price;
        if (price.recurring?.usage_type === 'metered') {
          return { price: x.stripePrice.id };
        }
        return { price: x.stripePrice.id, quantity: x.quantity };
      });

    const onetimeItems = prices
      .filter((x) => x.price.type !== 'recurring')
      .map((x) => ({ price: x.stripePrice.id, quantity: x.quantity }));

    const props: any = {
      currency: currency.symbol.toLowerCase(),
      customer: customer.id,
      items: recurringItems,
      add_invoice_items: onetimeItems,
      collection_method: 'charge_automatically',
      payment_behavior: 'default_incomplete',
      payment_settings: { save_default_payment_method: 'on_subscription' },
      metadata: {
        appPid: env.appPid,
        id: internal.id,
      },
      expand: ['latest_invoice.payment_intent', 'pending_setup_intent'],
    };

    if (trialInDays) {
      props.trial_period_days = trialInDays;
    } else if (trialEnd) {
      props.trial_end = trialEnd;
    }

    // Handle discounts for subscription
    const discounts = await getSubscriptionDiscounts(internal, method, currency);
    if (discounts.length > 0) {
      props.discounts = discounts;
      logger.info('Applied discounts to Stripe subscription', {
        subscription_id: internal.id,
        discounts: discounts.map((d) => d.coupon),
        discount_count: discounts.length,
      });
    }

    stripeSubscription = await client.subscriptions.create(props);
    logger.info('stripe subscription created', { local: internal.id, remote: stripeSubscription.id });

    await Promise.all(
      stripeSubscription.items.data.map(async (x: any) => {
        const item = prices.find((y) => y.stripePrice.id === x.price.id);
        const price = getSubscriptionItemPrice(item); // local
        let exist = await SubscriptionItem.findOne({
          where: { price_id: price.id, subscription_id: internal.id },
        });
        if (exist) {
          await exist.update({ metadata: { stripe_id: x.id, stripe_subscription_id: stripeSubscription.id } });
          await client.subscriptionItems.update(x.id, { metadata: { appPid: env.appPid, id: exist.id } });
          logger.info('stripe subscription items related', { local: exist.id, remote: x.id });
        } else {
          exist = await SubscriptionItem.create({
            livemode: stripeSubscription.livemode,
            price_id: price.id,
            quantity: x.quantity,
            subscription_id: internal.id,
            billing_thresholds: x.billing_threshold,
            metadata: {
              stripe_id: x.id,
              stripe_subscription_id: stripeSubscription.id,
            },
          });
          await client.subscriptionItems.update(x.id, { metadata: { appPid: env.appPid, id: exist.id } });
          logger.info('stripe subscription items mirrored', { local: exist.id, remote: x.id });
        }
      })
    );
  }
  return stripeSubscription;
}

export async function forwardUsageRecordToStripe(
  subscriptionItem: SubscriptionItem,
  updates: { quantity: number; timestamp: number; action: string }
) {
  if (!subscriptionItem.metadata?.stripe_id) {
    logger.info('skip usage record forwarded to stripe because no relation', {
      subscriptionItemId: subscriptionItem.id,
      updates,
    });
    return null;
  }

  const subscription = await Subscription.findByPk(subscriptionItem.subscription_id);
  if (!subscription) {
    return null;
  }

  const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
  if (!method) {
    return null;
  }
  if (method.type !== 'stripe') {
    return null;
  }

  const client = method.getStripeClient();
  const result = await client.subscriptionItems.createUsageRecord(subscriptionItem.metadata.stripe_id, {
    quantity: updates.quantity,
    timestamp: updates.timestamp,
    // @ts-ignore
    action: subscription.billing_thresholds?.amount_gte ? 'increment' : updates.action,
  });

  logger.info('usage record forwarded to stripe', { subscriptionItemId: subscriptionItem.id, updates, result });

  return result;
}

/**
 * Get all discounts associated with a subscription and create Stripe coupons
 */
async function getSubscriptionDiscounts(
  subscription: Subscription,
  method: PaymentMethod,
  currency: PaymentCurrency
): Promise<{ coupon: string }[]> {
  const discounts = (await Discount.findAll({
    where: {
      subscription_id: subscription.id,
    },
    include: [
      {
        model: Coupon,
        as: 'coupon',
        required: true,
        where: {
          valid: true,
        },
      },
    ],
  })) as (Discount & { coupon: Coupon })[];

  const stripeDiscounts = [];

  for (const discount of discounts) {
    try {
      const stripeCouponId = await createStripeSubscriptionCoupon(discount.coupon, subscription.id, method, currency);
      stripeDiscounts.push({ coupon: stripeCouponId });

      logger.info('Created Stripe coupon for subscription', {
        local_coupon: discount.coupon_id,
        stripe_coupon: stripeCouponId,
        subscription_id: subscription.id,
      });
    } catch (error) {
      logger.error('Failed to create Stripe coupon for subscription', {
        discount_id: discount.id,
        coupon_id: discount.coupon_id,
        subscription_id: subscription.id,
        error: error.message,
      });
    }
  }

  return stripeDiscounts;
}

/**
 * Update subscription discounts by managing existing and new discounts
 * Since Stripe doesn't allow direct discount updates on subscriptions,
 * we need to use discount deletion and application approach
 */
async function updateSubscriptionDiscounts(
  client: any,
  stripeSubscription: any,
  subscription: Subscription,
  method: PaymentMethod,
  currency: PaymentCurrency
): Promise<void> {
  try {
    const newDiscounts = await getSubscriptionDiscounts(subscription, method, currency);
    const existingDiscounts = stripeSubscription.discounts || [];

    const existingCouponIds = existingDiscounts.map((d: any) => d.coupon?.id).filter(Boolean);
    const newCouponIds = newDiscounts.map((d: any) => d.coupon);

    const hasChanges =
      existingCouponIds.length !== newCouponIds.length ||
      !existingCouponIds.every((id: string) => newCouponIds.includes(id));

    if (!hasChanges) {
      logger.info('No discount changes needed for subscription', {
        subscription_id: subscription.id,
        stripe_subscription_id: stripeSubscription.id,
      });
      return;
    }

    // Remove existing discounts (Stripe subscriptions typically have only one discount)
    if (existingDiscounts.length > 0) {
      try {
        await client.subscriptions.deleteDiscount(stripeSubscription.id);
        logger.info('Removed existing discounts from subscription', {
          subscription_id: subscription.id,
          stripe_subscription_id: stripeSubscription.id,
          removed_count: existingDiscounts.length,
        });
      } catch (error) {
        logger.error('Failed to remove discounts from subscription', {
          subscription_id: subscription.id,
          stripe_subscription_id: stripeSubscription.id,
          error: error.message,
        });
      }
    }

    // Apply new discounts using the discounts field
    if (newDiscounts.length > 0) {
      try {
        await client.subscriptions.update(stripeSubscription.id, {
          discounts: newDiscounts,
        });

        logger.info('Applied new discounts to subscription', {
          subscription_id: subscription.id,
          stripe_subscription_id: stripeSubscription.id,
          discounts: newDiscounts.map((d) => d.coupon),
          discount_count: newDiscounts.length,
        });
      } catch (error) {
        logger.error('Failed to apply discounts to subscription', {
          subscription_id: subscription.id,
          stripe_subscription_id: stripeSubscription.id,
          discounts: newDiscounts,
          error: error.message,
        });
      }
    }
  } catch (error) {
    logger.error('Failed to update subscription discounts', {
      subscription_id: subscription.id,
      stripe_subscription_id: stripeSubscription.id,
      error: error.message,
    });
    throw error;
  }
}

/**
 * Create a Stripe coupon specifically for a subscription with max_redemptions = 1
 */
async function createStripeSubscriptionCoupon(
  localCoupon: Coupon,
  subscriptionId: string,
  method: PaymentMethod,
  currency?: PaymentCurrency
): Promise<string> {
  const client = method.getStripeClient();

  // Check if coupon already exists for this subscription
  const existingCouponId = `${localCoupon.id}_sub_${subscriptionId.slice(-8)}`;
  try {
    const existingCoupon = await client.coupons.retrieve(existingCouponId);
    if (existingCoupon) {
      logger.info('Stripe coupon already exists for subscription', {
        local_coupon_id: localCoupon.id,
        stripe_coupon_id: existingCoupon.id,
        subscription_id: subscriptionId,
      });
      return existingCoupon.id;
    }
  } catch (error) {
    logger.error('Failed to retrieve Stripe coupon for subscription', {
      local_coupon_id: localCoupon.id,
      subscription_id: subscriptionId,
      error,
    });
  }
  // Prepare coupon data
  const couponData: any = {
    id: existingCouponId, // Use predictable ID
    name: `sub_coupon_${localCoupon.id}`,
    duration: localCoupon.duration as 'once' | 'forever' | 'repeating',
    redeem_by: Math.floor((new Date().getTime() + 24 * 60 * 60 * 1000) / 1000),
    metadata: {
      appPid: env.appPid,
      local_coupon_id: localCoupon.id,
      subscription_id: subscriptionId,
      created_for: 'subscription',
    },
  };

  // Add duration in months for repeating coupons
  if (localCoupon.duration === 'repeating' && localCoupon.duration_in_months) {
    couponData.duration_in_months = localCoupon.duration_in_months;
  }

  // Add discount amount - either percent_off or amount_off
  if (localCoupon.percent_off && localCoupon.percent_off > 0) {
    couponData.percent_off = localCoupon.percent_off;
  } else if (localCoupon.amount_off && localCoupon.amount_off !== '0') {
    couponData.amount_off = Number(localCoupon.amount_off || '0');

    // Get currency for fixed amount discounts
    let targetCurrency = currency;
    if (!targetCurrency && localCoupon.currency_id) {
      targetCurrency = (await PaymentCurrency.findByPk(localCoupon.currency_id)) || undefined;
    }

    if (targetCurrency) {
      couponData.currency = targetCurrency.symbol.toLowerCase();
    } else {
      // Fallback to USD if no currency specified
      couponData.currency = 'usd';
      logger.warn('No currency specified for fixed amount coupon, defaulting to USD', {
        local_coupon_id: localCoupon.id,
        subscription_id: subscriptionId,
      });
    }
  } else {
    // Default to 0% discount if neither percent_off nor amount_off is set
    couponData.percent_off = 0;
    logger.warn('Coupon has no discount amount set, defaulting to 0%', {
      local_coupon_id: localCoupon.id,
      subscription_id: subscriptionId,
    });
  }

  try {
    const stripeCoupon = await client.coupons.create(couponData);

    logger.info('Created Stripe coupon for subscription', {
      local_coupon_id: localCoupon.id,
      stripe_coupon_id: stripeCoupon.id,
      subscription_id: subscriptionId,
      coupon_name: stripeCoupon.name,
      duration: stripeCoupon.duration,
      percent_off: stripeCoupon.percent_off,
      amount_off: stripeCoupon.amount_off,
      currency: stripeCoupon.currency,
      max_redemptions: stripeCoupon.max_redemptions,
    });

    return stripeCoupon.id;
  } catch (error) {
    logger.error('Failed to create Stripe coupon for subscription', {
      local_coupon_id: localCoupon.id,
      subscription_id: subscriptionId,
      coupon_data: couponData,
      error,
    });
    throw error;
  }
}

export async function batchHandleStripeInvoices() {
  const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
  if (stripeMethods.length === 0) {
    return;
  }

  const stripeInvoices = await Invoice.findAll({
    where: {
      status: ['draft', 'open', 'finalized'],
      'metadata.stripe_id': { [Op.not]: null },
    },
  });

  for (const invoice of stripeInvoices) {
    const stripeInvoiceId = invoice.metadata?.stripe_id;
    if (!stripeInvoiceId) {
      continue;
    }

    const method = stripeMethods.find((m) => m.livemode === invoice.livemode);
    if (!method) {
      continue;
    }

    const client = method.getStripeClient();
    try {
      let exist = await client.invoices.retrieve(stripeInvoiceId);
      if (exist) {
        if (exist.status === 'draft') {
          exist = await client.invoices.finalizeInvoice(stripeInvoiceId);
          logger.info('stripe invoice finalized', { local: invoice.id, stripe: stripeInvoiceId });
        }
        if (exist.status !== 'paid') {
          exist = await client.invoices.pay(stripeInvoiceId);
          logger.info('stripe invoice payment requested', { local: invoice.id, stripe: stripeInvoiceId });
        }

        await syncStripeInvoice(invoice);

        if (invoice.payment_intent_id) {
          const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
          await syncStripePayment(paymentIntent!);
        }
      } else {
        await Invoice.destroy({ where: { id: invoice.id } });
        logger.warn('stripe invoice purged', { local: invoice.id, stripe: stripeInvoiceId });
      }
    } catch (error) {
      if (error.message.includes('No such invoice')) {
        await PaymentIntent.destroy({ where: { invoice_id: invoice.id } });
        await Invoice.destroy({ where: { id: invoice.id } });
        logger.warn('stripe invoice and related payment intent purged', { local: invoice.id, stripe: stripeInvoiceId });
      } else {
        logger.error('stripe invoice finalize error', error);
      }
    }

    await sleep(2000);
  }
}

export async function batchHandleStripeSubscriptions() {
  const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
  if (stripeMethods.length === 0) {
    return;
  }

  const subscriptions = await Subscription.findAll({
    where: {
      status: { [Op.notIn]: ['canceled', 'incomplete', 'incomplete_expired'] },
      'payment_details.stripe.subscription_id': { [Op.not]: null },
    },
  });

  for (const subscription of subscriptions) {
    const subscriptionId = subscription.payment_details?.stripe?.subscription_id;
    if (!subscriptionId) {
      continue;
    }

    const method = stripeMethods.find((m) => m.livemode === subscription.livemode);
    if (!method) {
      continue;
    }

    const client = method.getStripeClient();
    try {
      const exist = await client.subscriptions.retrieve(subscriptionId);
      if (exist) {
        const fields = [
          'cancel_at',
          'cancel_at_period_end',
          'canceled_at',
          'current_period_start',
          'current_period_end',
        ];
        if (subscription.payment_settings?.payment_method_types?.includes('stripe')) {
          fields.push('pause_collection');
        }
        const updates: any = pick(exist, fields);
        if (exist.latest_invoice) {
          const invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': exist.latest_invoice } });
          if (invoice) {
            updates.latest_invoice_id = invoice.id;
          }
        }

        await subscription.update(updates);
        logger.info('stripe subscription synced', { local: subscription.id, stripe: subscriptionId, updates });
      } else {
        logger.warn('stripe subscription missing', { local: subscription.id, stripe: subscriptionId });
      }
    } catch (error) {
      logger.error('stripe subscription sync error', { error, local: subscription.id, stripe: subscriptionId });
      if (error.message.includes('No such subscription')) {
        await subscription.update({
          payment_details: {
            ...subscription.payment_details,
            stripe: omit(subscription.payment_details?.stripe, ['subscription_id']),
          },
        });
      }
    }

    await sleep(2000);
  }
}

export async function batchHandleStripePayments() {
  const stripeMethods = await PaymentMethod.findAll({ where: { type: 'stripe' } });
  if (stripeMethods.length === 0) {
    return;
  }

  const stripePayments = await PaymentIntent.findAll({
    where: {
      status: ['requires_payment_method'],
      'metadata.stripe_id': { [Op.not]: null },
    },
  });

  for (const payment of stripePayments) {
    const stripePaymentId = payment.metadata?.stripe_id;
    if (!stripePaymentId) {
      continue;
    }

    const method = stripeMethods.find((m) => m.livemode === payment.livemode);
    if (!method) {
      continue;
    }

    const client = method.getStripeClient();
    try {
      const exist = await client.paymentIntents.retrieve(stripePaymentId);
      if (exist) {
        await syncStripePayment(payment);
      }
    } catch (error) {
      if (error.message.includes('No such payment')) {
        await PaymentIntent.destroy({ where: { id: payment.id } });
        logger.warn('stripe payment intent purged', { local: payment.id, stripe: stripePaymentId });
      } else {
        logger.error('stripe payment sync error', error);
      }
    }

    await sleep(1000);
  }
}

export async function ensureStripeSetupIntentForAutoRecharge(
  customer: Customer,
  method: PaymentMethod,
  autoRechargeConfig: AutoRechargeConfig,
  forceReauthorize: boolean = false
) {
  const client = method.getStripeClient();
  if (autoRechargeConfig.payment_details?.stripe?.setup_intent_id && !forceReauthorize) {
    const setupIntent = await client.setupIntents.retrieve(autoRechargeConfig.payment_details.stripe.setup_intent_id);
    return setupIntent;
  }
  const stripeCustomer = await ensureStripeCustomer(customer, method);

  const setupIntent = await client.setupIntents.create({
    customer: stripeCustomer.id,
    payment_method_types: ['card'],
    usage: 'off_session',
    metadata: {
      appPid: env.appPid,
      auto_recharge_config_id: autoRechargeConfig.id,
      customer_id: customer.id,
    },
  });

  logger.info('stripe setup intent created for auto recharge', {
    customerId: customer.id,
    autoRechargeConfigId: autoRechargeConfig.id,
    setupIntentId: setupIntent.id,
  });

  return setupIntent;
}

export async function ensureStripeSetupIntentForInvoicePayment(
  customer: Customer,
  method: PaymentMethod,
  metadata: Record<string, string>
) {
  const client = method.getStripeClient();
  const stripeCustomer = await ensureStripeCustomer(customer, method);

  const setupIntent = await client.setupIntents.create({
    customer: stripeCustomer.id,
    payment_method_types: ['card'],
    usage: 'off_session',
    metadata: {
      appPid: env.appPid,
      customer_id: customer.id,
      action: 'pay_overdue_batch',
      ...metadata,
    },
  });

  logger.info('stripe setup intent created for invoice payment', {
    customerId: customer.id,
    setupIntentId: setupIntent.id,
    metadata,
  });

  return setupIntent;
}

export async function updateAutoRechargeConfigPaymentMethod(params: {
  stripePaymentMethodId: string;
  autoRechargeConfig: AutoRechargeConfig;
}) {
  const { stripePaymentMethodId, autoRechargeConfig } = params;

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

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

    const stripeClient = paymentMethod.getStripeClient();

    const stripePaymentMethod = await stripeClient.paymentMethods.retrieve(stripePaymentMethodId);

    const paymentSettings = {
      payment_method_types: ['stripe'],
      payment_method_options: {
        ...(autoRechargeConfig.payment_settings?.payment_method_options || {}),
        stripe: {
          payer: stripePaymentMethodId,
          card_last4: stripePaymentMethod.card?.last4,
          card_brand: stripePaymentMethod.card?.brand,
          exp_time: `${stripePaymentMethod.card?.exp_month}/${stripePaymentMethod.card?.exp_year}`,
        },
      },
    };

    await autoRechargeConfig.update({
      payment_settings: paymentSettings,
      payment_details: {
        ...autoRechargeConfig.payment_details,
        stripe: {
          ...(autoRechargeConfig.payment_details?.stripe || {}),
          payment_method_id: stripePaymentMethodId,
        },
      },
    });
    return autoRechargeConfig;
  } catch (error: any) {
    logger.error('Failed to update auto recharge config payment method', {
      error: error.message,
      configId: autoRechargeConfig.id,
      paymentMethod: stripePaymentMethodId,
    });
    throw error;
  }
}

export async function updateGroupSubscriptionsPaymentMethod(params: {
  stripePaymentMethodId: string;
  subscriptionIds: string[];
}) {
  const { stripePaymentMethodId, subscriptionIds } = params;
  try {
    const updates = subscriptionIds.map(async (subId) => {
      const subscription = (await Subscription.findByPk(subId, {
        include: [{ model: PaymentMethod, as: 'paymentMethod' }],
      })) as Subscription & { paymentMethod: PaymentMethod };
      if (!subscription || !subscription.paymentMethod || !subscription.payment_details?.stripe?.subscription_id) {
        return null;
      }
      const client = subscription.paymentMethod.getStripeClient();
      let stripeSub = await client.subscriptions.retrieve(subscription.payment_details.stripe.subscription_id, {
        expand: ['latest_invoice', 'latest_invoice.payment_intent'],
      });
      if (stripeSub.status !== 'incomplete') {
        return null;
      }
      const updatedSub = await client.subscriptions.update(stripeSub.id, {
        default_payment_method: stripePaymentMethodId,
        payment_behavior: 'default_incomplete',
      });

      stripeSub = await client.subscriptions.retrieve(updatedSub.id, {
        expand: ['latest_invoice', 'latest_invoice.payment_intent'],
      });
      // if the latest invoice is not paid, pay it
      // @ts-ignore
      if (stripeSub.latest_invoice && stripeSub.latest_invoice?.status !== 'paid') {
        // @ts-ignore
        await client.invoices.pay(stripeSub.latest_invoice.id);
      }
      return updatedSub;
    });

    const updatedSubscriptions = await Promise.all(updates);
    logger.info('Updated group subscriptions payment method', {
      paymentMethod: stripePaymentMethodId,
      subscriptions: subscriptionIds,
    });

    return updatedSubscriptions;
  } catch (error) {
    logger.error('Failed to update group subscriptions payment method', {
      error,
      paymentMethod: stripePaymentMethodId,
      subscriptionIds,
    });
    throw error;
  }
}

export async function createStripeInvoiceForAutoRecharge(params: {
  autoRechargeConfig: AutoRechargeConfig;
  customer: Customer;
  paymentMethod: PaymentMethod;
  currency: PaymentCurrency;
  invoice: Invoice;
}) {
  const { autoRechargeConfig, customer, paymentMethod, currency, invoice } = params;
  const client = paymentMethod.getStripeClient();

  const stripePaymentMethodId = autoRechargeConfig.payment_settings?.payment_method_options?.stripe?.payer;
  if (!stripePaymentMethodId) {
    throw new Error('Stripe payment method not found');
  }

  try {
    // Ensure stripe customer exists
    const stripeCustomer = await ensureStripeCustomer(customer, paymentMethod);

    if (invoice.metadata?.stripe_id) {
      const existInvoice = await client.invoices.retrieve(invoice.metadata?.stripe_id);
      if (existInvoice) {
        logger.info('Stripe invoice already exists, skipping', {
          localInvoiceId: invoice.id,
        });
        return existInvoice;
      }
    }

    const stripeInvoice = await client.invoices.create({
      customer: stripeCustomer.id,
      currency: currency.symbol.toLowerCase(),
      description: invoice.description || 'Auto recharge',
      collection_method: 'charge_automatically',
      auto_advance: true,
      default_payment_method: stripePaymentMethodId,
      metadata: {
        appPid: env.appPid,
        recharge_id: autoRechargeConfig.id,
        id: invoice.id,
      },
    });

    logger.info('Stripe invoice created for auto recharge', {
      stripeInvoiceId: stripeInvoice.id,
      customerId: customer.id,
    });

    await invoice.update({
      metadata: {
        ...invoice.metadata,
        stripe_id: stripeInvoice.id,
      },
    });

    logger.info('Stripe invoice updated', {
      localInvoiceId: invoice.id,
      stripeInvoiceId: stripeInvoice.id,
    });

    // Create invoice items from local invoice items
    const invoiceItems = await InvoiceItem.findAll({ where: { invoice_id: invoice.id } });
    for (const item of invoiceItems) {
      const price = await Price.findByPk(item.price_id);
      if (!price) {
        continue;
      }
      const stripePrice = await ensureStripePrice(price as Price, paymentMethod, currency);
      const stripeInvoiceItem = await client.invoiceItems.create({
        customer: stripeCustomer.id,
        invoice: stripeInvoice.id,
        price: stripePrice.id,
        quantity: item.quantity,
        description: item.description || price.nickname || '',
        metadata: {
          appPid: env.appPid,
          invoice_item_id: item.id,
        },
      });

      logger.info('Stripe invoice item created', {
        localItemId: item.id,
        stripeInvoiceId: stripeInvoice.id,
        priceId: price.id,
        quantity: item.quantity,
      });
      await item.update({
        metadata: {
          ...item.metadata,
          stripe_id: stripeInvoiceItem.id,
        },
      });
    }

    // Finalize and pay the invoice automatically
    const finalizedInvoice = await client.invoices.finalizeInvoice(stripeInvoice.id);
    logger.info('Stripe invoice finalized', {
      localInvoiceId: invoice.id,
      stripeInvoiceId: stripeInvoice.id,
    });

    // Attempt automatic payment
    if (finalizedInvoice.status === 'open') {
      const paidInvoice = await client.invoices.pay(stripeInvoice.id);
      logger.info('Stripe invoice payment attempted', {
        localInvoiceId: invoice.id,
        stripeInvoiceId: stripeInvoice.id,
        status: paidInvoice.status,
      });
      return paidInvoice;
    }

    return finalizedInvoice;
  } catch (error: any) {
    logger.error('Failed to create Stripe invoice for auto recharge', {
      localInvoiceId: invoice.id,
      customerId: customer.id,
      error,
    });
    throw error;
  }
}
