import { component } from '@blocklet/sdk';
import type { LiteralUnion } from 'type-fest';
import { withQuery } from 'ufo';

import { BN, fromUnitToToken } from '@ocap/util';
import { Op, type WhereOptions } from 'sequelize';
import { cloneDeep, pick } from 'lodash';
import {
  Customer,
  Invoice,
  InvoiceItem,
  PaymentCurrency,
  PaymentIntent,
  PaymentMethod,
  PaymentSettings,
  Price,
  PriceQuote,
  Product,
  Refund,
  SetupIntent,
  SimpleCustomField,
  Subscription,
  SubscriptionItem,
  TaxRate,
  TInvoice,
  TLineItemExpanded,
  UsageRecord,
  Lock,
  DiscountAmount,
} from '../store/models';
import { getConnectQueryParam } from './util';
import { expandLineItems } from './session';
import dayjs from './dayjs';
import {
  getSubscriptionCycleAmount,
  getSubscriptionCycleSetup,
  getSubscriptionItemPrice,
  getSubscriptionStakeAmountSetup,
  isSubscriptionOverdraftProtectionEnabled,
} from './subscription';
import logger from './logger';
import { ensureOverdraftProtectionPrice } from './overdraft-protection';

import { emitAsync } from './event';
import { getPriceUintAmountByCurrency } from './price';
import { generateQuotesForInvoiceItems } from './invoice-quote';
import { getExchangeRateService, getExchangeRateSymbol } from './exchange-rate';
import { getQuoteService } from './quote-service';
import type { ChainType } from '../store/models';

export function getCustomerInvoicePageUrl({
  invoiceId,
  userDid,
  locale = 'en',
  action = '',
}: {
  userDid: string;
  invoiceId: string;
  locale: LiteralUnion<'en' | 'zh', string>;
  action?: LiteralUnion<'pay', string>;
}) {
  return component.getUrl(
    withQuery(`customer/invoice/${invoiceId}`, {
      locale,
      action,
      ...getConnectQueryParam({ userDid }),
    })
  );
}

export async function getOneTimeProductInfo(invoiceId: string, paymentCurrency: PaymentCurrency) {
  try {
    const doc = await Invoice.findOne({
      where: { id: invoiceId },
      include: [{ model: InvoiceItem, as: 'lines' }],
    });
    if (!doc) {
      throw new Error(`Invoice not found: ${invoiceId}`);
    }
    const json = doc.toJSON();
    const products = (await Product.findAll()).map((x) => x.toJSON());
    const prices = (await Price.findAll()).map((x) => x.toJSON());
    // @ts-ignore
    expandLineItems(json.lines, products, prices);
    const oneTimePaymentInfo: Array<{
      productName: string;
      paymentInfo: string;
      quantity: number;
    }> = [];
    // @ts-ignore
    (json.lines || []).forEach((x) => {
      const { recurring, product } = x.price;
      if (!recurring) {
        // it's one-time product
        oneTimePaymentInfo.push({
          productName: product?.name,
          paymentInfo: `${fromUnitToToken(x.amount, paymentCurrency.decimal)} ${paymentCurrency.symbol}`,
          quantity: x.quantity,
        });
      }
    });
    return oneTimePaymentInfo;
  } catch (err) {
    console.error(`Error in getOneTimeProductInfo for invoice ${invoiceId}:`, err);
    return [];
  }
}

export async function getInvoiceShouldPayTotal(invoice: Invoice) {
  try {
    const subscription = await Subscription.findByPk(invoice.subscription_id);
    if (!subscription) {
      throw new Error(`Subscription not found: ${invoice.subscription_id}`);
    }

    const paymentCurrency = await PaymentCurrency.findByPk(invoice.currency_id);
    if (!paymentCurrency) {
      throw new Error(`Payment currency not found: ${invoice.currency_id}`);
    }

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

    const subscriptionItems = await SubscriptionItem.findAll({
      where: { subscription_id: subscription.id },
    });
    const invoiceItems = await InvoiceItem.findAll({
      where: { invoice_id: invoice.id },
    });
    const invoiceQuoteIds = invoiceItems
      .map((item) => item.metadata?.quote_id)
      .filter((id): id is string => typeof id === 'string' && id.length > 0);
    const invoiceQuotes = invoiceQuoteIds.length ? await PriceQuote.findAll({ where: { id: invoiceQuoteIds } }) : [];
    const invoiceQuotesById = new Map(invoiceQuotes.map((quote) => [quote.id, quote]));
    const invoiceQuoteBySubscriptionItemId = new Map<string, { quotedAmount?: string; amount?: string }>();
    invoiceItems.forEach((item) => {
      if (!item.subscription_item_id) {
        return;
      }
      const quoteId = typeof item.metadata?.quote_id === 'string' ? item.metadata?.quote_id : undefined;
      const quote = quoteId ? invoiceQuotesById.get(quoteId) : undefined;
      const quotedAmount = quote?.quoted_amount || (item.metadata as any)?.quote?.quoted_amount;
      invoiceQuoteBySubscriptionItemId.set(item.subscription_item_id, {
        quotedAmount,
        amount: item.amount,
      });
    });

    let expandedItems = await Price.expand(
      subscriptionItems.map((x) => ({ id: x.id, price_id: x.price_id, quantity: x.quantity })),
      { product: true }
    );

    const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, invoice.period_start);
    let offset = 0;
    let filterFunc = (item: any) => item;
    switch (invoice.billing_reason) {
      case 'subscription_cancel':
        filterFunc = (item: any) => item.price?.recurring?.usage_type === 'metered';
        break;
      case 'subscription_cycle':
        filterFunc = (item: any) => item.price?.type === 'recurring';
        offset = setup.cycle / 1000;
        break;
      default:
        filterFunc = () => true;
    }
    const previousPeriodStart = setup.period.start - offset;
    const previousPeriodEnd = setup.period.end - offset;
    expandedItems = await Promise.all(
      expandedItems.filter(filterFunc).map(async (item: any) => {
        const { price } = item;
        if (price?.recurring?.usage_type === 'metered') {
          const rawQuantity = await UsageRecord.getSummary({
            id: item.id,
            start: previousPeriodStart,
            end: previousPeriodEnd,
            method: price.recurring?.aggregate_usage,
            dryRun: true,
            searchBilled: false,
          });
          item.quantity = price.transformQuantity(rawQuantity);
        }
        return item;
      })
    );

    if (invoiceQuoteBySubscriptionItemId.size > 0) {
      expandedItems = expandedItems.map((item: any) => {
        const { price } = item;
        if (price?.pricing_type !== 'dynamic') {
          return item;
        }
        const quoteInfo = invoiceQuoteBySubscriptionItemId.get(item.id);
        if (!quoteInfo) {
          return item;
        }
        const quotedAmount = quoteInfo.quotedAmount || quoteInfo.amount;
        if (!quotedAmount) {
          return item;
        }
        return {
          ...item,
          custom_amount: quotedAmount,
          quoted_amount: quotedAmount,
        };
      });
    }

    const baseAmount = getSubscriptionCycleAmount(expandedItems, subscription.currency_id);
    let shouldPayTotal = baseAmount?.total || invoice.total;

    // Apply discount if exists
    if (baseAmount?.total && invoice.total_discount_amounts && invoice.total_discount_amounts.length > 0) {
      const totalDiscountAmount = invoice.total_discount_amounts.reduce((sum, discount) => {
        return new BN(sum).add(new BN(discount.amount.toString())).toString();
      }, '0');

      shouldPayTotal = new BN(shouldPayTotal).sub(new BN(totalDiscountAmount)).toString();

      // Ensure the final amount doesn't go below zero
      if (new BN(shouldPayTotal).lt(new BN('0'))) {
        shouldPayTotal = '0';
      }
    }

    return shouldPayTotal;
  } catch (err) {
    console.error(`Error in getInvoiceShouldPayTotal for invoice ${invoice.id}:`, err);
    return invoice.total;
  }
}

export async function getReturnStakeInvoices(subscription: Subscription) {
  const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
  const returnStakeInvoice: Invoice[] = [];
  const refunds = await Refund.findAll({
    where: { subscription_id: subscription.id, status: 'succeeded', type: 'stake_return' },
    include: [
      { model: Invoice, as: 'invoice' },
      { model: Customer, as: 'customer' },
      { model: PaymentCurrency, as: 'paymentCurrency' },
    ],
  });

  await Promise.all(
    refunds.map(async (r: any) => {
      const invoice =
        r.invoice ||
        (await Invoice.findOne({
          where: {
            subscription_id: subscription.id,
            currency_id: r.currency_id,
          },
          order: [['created_at', 'ASC']],
        }));
      returnStakeInvoice.push({
        id: r.id,
        status: 'paid',
        billing_reason: 'return_stake',
        description: 'Return Subscription staking',
        total: r.amount,
        amount_due: '0',
        amount_paid: r.amount,
        amount_remaining: '0',
        subscription_id: subscription.id,
        paid: true,
        ...pick(invoice, ['number', 'auto_advance', 'period_start', 'period_end']),
        ...pick(r, ['created_at', 'updated_at', 'currency_id', 'customer_id']),
        // @ts-ignore
        paymentCurrency: r.paymentCurrency,
        paymentMethod: method,
        customer: r.customer,
        metadata: {
          payment_details: {
            arcblock: {
              tx_hash: r?.payment_details?.arcblock?.tx_hash,
              payer: r?.payment_details?.arcblock?.payer,
            },
          },
        },
      });
    })
  );
  return returnStakeInvoice;
}

export async function getSetupInvoice(
  subscription: Subscription,
  paymentMethod: PaymentMethod,
  currencyId: string,
  stakingProps: { tx_hash: string; payer: string; address: string }
) {
  const currency = await PaymentCurrency.findByPk(currencyId);
  if (!currency) {
    return null;
  }
  if (!stakingProps?.tx_hash) {
    return null;
  }
  if (paymentMethod?.type !== 'arcblock') {
    return null;
  }
  const firstInvoice = await Invoice.findOne({
    where: { subscription_id: subscription.id, currency_id: currency.id },
    order: [['created_at', 'ASC']],
    include: [{ model: Customer, as: 'customer' }],
  });
  if (!firstInvoice) {
    return null;
  }
  const stakeAmountResult = await getSubscriptionStakeAmountSetup(subscription, paymentMethod, stakingProps.tx_hash);
  // @ts-ignore
  const stakeAmount = stakeAmountResult?.[currency?.contract] || '0';
  return {
    id: stakingProps.address as string,
    status: 'paid',
    description: 'Subscription staking',
    billing_reason: 'stake',
    total: stakeAmount,
    amount_due: '0',
    amount_paid: stakeAmount,
    amount_remaining: '0',
    ...pick(firstInvoice, [
      'number',
      'paid',
      'auto_advance',
      'currency_id',
      'customer_id',
      'subscription_id',
      'period_start',
      'period_end',
      'created_at',
      'updated_at',
    ]),
    // @ts-ignore
    paymentCurrency: currency,
    paymentMethod,
    // @ts-ignore
    customer: firstInvoice?.customer,
    metadata: {
      payment_details: {
        arcblock: {
          tx_hash: stakingProps?.tx_hash,
          payer: stakingProps?.payer,
        },
      },
    },
  };
}

export async function getStakingInvoices(subscription: Subscription): Promise<Invoice[]> {
  const method = await PaymentMethod.findOne({ where: { type: 'arcblock', livemode: subscription.livemode } });
  if (!method) {
    return [];
  }
  const stakeInvoices = await Invoice.findAll({
    where: { subscription_id: subscription.id, billing_reason: 'stake', status: 'paid' },
    include: [
      { model: Customer, as: 'customer' },
      { model: PaymentCurrency, as: 'paymentCurrency' },
      { model: PaymentMethod, as: 'paymentMethod' },
    ],
  });
  const invoices = cloneDeep(stakeInvoices);
  const setups = await SetupIntent.findAll({
    where: {
      customer_id: subscription.customer_id,
      payment_method_id: method?.id,
      status: 'succeeded',
      [Op.and]: [
        { metadata: { [Op.not]: null } },
        { 'metadata.subscription_id': subscription.id },
        { setup_details: { [Op.not]: null } },
        { 'setup_details.arcblock': { [Op.not]: null } },
        { 'setup_details.arcblock.staking': { [Op.not]: null } },
      ] as any,
    },
  });
  if (setups.length === 0) {
    // No payment change, use the first invoice
    if (stakeInvoices.length > 0) {
      return stakeInvoices;
    }
    const newInvoice = await getSetupInvoice(subscription, method, subscription.currency_id, {
      tx_hash: subscription.payment_details?.arcblock?.staking?.tx_hash!,
      payer: subscription.payment_details?.arcblock?.payer!,
      address: subscription.payment_details?.arcblock?.staking?.address!,
    });
    // @ts-ignore
    return [newInvoice].filter(Boolean);
  }

  await Promise.all(
    setups.map(async (setup: SetupIntent, index: number) => {
      if (
        index === 0 &&
        setup.metadata?.from_currency &&
        !stakeInvoices.find((x) => x.currency_id === setup?.metadata?.from_currency)
      ) {
        const paymentDetails =
          setup?.metadata?.from_payment_details?.arcblock || subscription.payment_details?.arcblock;
        const fromMethod = await PaymentMethod.findByPk(setup?.metadata?.from_payment_method_id);
        const newInvoice = await getSetupInvoice(subscription, fromMethod!, setup.metadata?.from_currency, {
          tx_hash: paymentDetails?.staking?.tx_hash!,
          payer: paymentDetails?.payer!,
          address: paymentDetails?.staking?.address!,
        });
        if (newInvoice) {
          // @ts-ignore
          invoices.push(newInvoice);
        }
      }
      if (setup.metadata?.to_currency && !stakeInvoices.find((x) => x.currency_id === setup?.metadata?.to_currency)) {
        const newInvoice = await getSetupInvoice(subscription, method, setup.metadata?.to_currency, {
          tx_hash: setup.setup_details?.arcblock?.staking?.tx_hash!,
          payer: setup.setup_details?.arcblock?.payer!,
          address: setup.setup_details?.arcblock?.staking?.address!,
        });
        if (newInvoice) {
          // @ts-ignore
          invoices.push(newInvoice);
        }
      }
    })
  );
  return invoices;
}

type BaseInvoiceProps = {
  customer: Customer;
  subscription?: Subscription | null;
  currency_id: string;
  livemode: boolean;
  period_start: number;
  period_end: number;
  status?: string;
  billing_reason: string;
  description: string;
  statement_descriptor?: string;
  total: string;
  subtotal?: string;
  amount_due?: string;
  amount_paid?: string;
  amount_remaining?: string;
  default_payment_method_id: string;
  payment_intent_id?: string;
  checkout_session_id?: string;
  metadata?: Record<string, any>;
  // Discount fields
  discounts?: string[];
  total_discount_amounts?: DiscountAmount[];
  items?: Array<{
    price_id: string;
    amount: string;
    quantity: number;
    description: string;
    period?: {
      start: number;
      end: number;
    };
    metadata?: Record<string, any>;
    subscription_item_id?: string;
    // Discount fields for InvoiceItem
    discountable?: boolean;
    discount_amounts?: DiscountAmount[];
  }>;
  auto_advance?: boolean;
  paid?: boolean;
  paid_out_of_band?: boolean;
  payment_settings?: PaymentSettings;
  footer?: string;
  custom_fields?: SimpleCustomField[];
  starting_token_balance?: Record<string, string>;
  ending_token_balance?: Record<string, string>;
  subtotal_excluding_tax?: string;
  collection_method?: 'charge_automatically' | 'send_invoice';
  id?: string;
  quote_id?: string;
};

async function createInvoiceWithItems(props: BaseInvoiceProps): Promise<{
  invoice: Invoice;
  items: InvoiceItem[];
}> {
  const {
    customer,
    subscription,
    currency_id: currencyId,
    livemode,
    period_start: periodStart,
    period_end: periodEnd,
    status = 'open',
    billing_reason: billingReason,
    description,
    statement_descriptor: statementDescriptor = '',
    total,
    amount_due: amountDue = total,
    amount_paid: amountPaid = '0',
    amount_remaining: amountRemaining = total,
    default_payment_method_id: paymentMethodId,
    payment_intent_id: paymentIntentId = '',
    checkout_session_id: checkoutSessionId = '',
    items: itemsData,
    metadata = {},
    paid = false,
    auto_advance: autoAdvance = true,
    paid_out_of_band: paidOutOfBand = false,
    payment_settings: paymentSettings,
    ...extraProps
  } = props;

  const invoiceNumber = await customer.getInvoiceNumber();
  // create invoice
  const invoice = await Invoice.create({
    amount_shipping: '0',
    starting_balance: '0',
    ending_balance: '0',
    subtotal_excluding_tax: '0',
    starting_token_balance: {},
    ending_token_balance: {},
    attempt_count: 0,
    attempted: false,
    tax: '0',
    discounts: props.discounts || [],
    total_discount_amounts: props.total_discount_amounts || [],
    collection_method: 'charge_automatically',
    ...extraProps,
    livemode,
    number: invoiceNumber,
    description,
    statement_descriptor: statementDescriptor,
    period_start: periodStart,
    period_end: periodEnd,
    auto_advance: autoAdvance,
    paid,
    paid_out_of_band: paidOutOfBand,
    status,
    billing_reason: billingReason,
    currency_id: currencyId,
    customer_id: customer.id,
    default_payment_method_id: paymentMethodId,
    payment_intent_id: paymentIntentId,
    subscription_id: subscription?.id,
    checkout_session_id: checkoutSessionId,
    total,
    subtotal: props.subtotal || total,
    amount_due: amountDue,
    amount_paid: amountPaid,
    amount_remaining: amountRemaining,

    customer_address: customer.address,
    customer_email: customer.email,
    customer_name: customer.name,
    customer_phone: customer.phone,

    effective_at: dayjs().unix(),
    status_transitions: {
      finalized_at: dayjs().unix(),
    },
    payment_settings: paymentSettings || subscription?.payment_settings,
    metadata,
  });

  if (!itemsData) {
    return { invoice, items: [] };
  }

  // Generate quotes for dynamic pricing items (subscription invoices)
  const enrichedItemsData = subscription
    ? await generateQuotesForInvoiceItems({
        invoiceId: invoice.id,
        items: itemsData,
        currencyId,
      })
    : itemsData;

  // create invoice items
  const items = await Promise.all(
    enrichedItemsData.map(async (item) => {
      // Match tax rate for this specific item
      let taxRateId: string | undefined;
      if (customer.address?.country && item.price_id) {
        try {
          const price = await Price.findByPk(item.price_id);
          if (price?.product_id) {
            const product = await Product.findByPk(price.product_id);
            if (product) {
              const taxRate = await TaxRate.findMatchingRate({
                country: customer.address.country,
                state: customer.address.state,
                postalCode: customer.address.postal_code,
                taxCode: product.tax_code,
                livemode,
              });
              taxRateId = taxRate?.id;
            }
          }
        } catch (error) {
          logger.error('Failed to match tax rate for invoice item:', error);
        }
      }

      return InvoiceItem.create({
        livemode,
        amount: item.amount,
        quantity: item.quantity,
        description: item.description,
        period: item.period,
        currency_id: currencyId,
        customer_id: customer.id,
        price_id: item.price_id,
        invoice_id: invoice.id,
        subscription_id: subscription?.id,
        subscription_item_id: item.subscription_item_id,
        tax_rate_id: taxRateId,
        discountable: item.discountable || false,
        discounts: [], // Keep as empty for now - this is legacy field
        discount_amounts:
          item.discount_amounts?.map((d) => ({ amount: new BN(d.amount || '0').toString(), discount: d.discount })) ||
          [],
        proration: false,
        proration_details: {},
        metadata: {
          ...(item.metadata || {}),
          // Include quote_id in metadata if it exists (for dynamic pricing)
          ...((item as any).quote_id && { quote_id: (item as any).quote_id }),
        },
      });
    })
  );

  return { invoice, items };
}

export async function ensureInvoiceAndItems({
  customer,
  currency,
  subscription,
  subscriptions,
  props,
  lineItems,
  trialing,
  metered,
  applyCredit = true,
}: {
  customer: Customer;
  currency: PaymentCurrency;
  subscription?: Subscription;
  subscriptions?: Subscription[];
  props: TInvoice;
  lineItems: TLineItemExpanded[];
  trialing: boolean; // do we have trialing
  metered: boolean; // is the quantity metered
  applyCredit?: boolean; // should we apply customer credit?
}): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
  // apply possible balance to invoice
  let remaining = props.total;
  let result = { starting: {}, ending: {} };
  if (applyCredit && props.total > '0' && !props.amount_remaining) {
    const balance = customer.getBalanceToApply(currency.id, props.total);
    result = await customer.decreaseTokenBalance(currency.id, balance);
    remaining = new BN(props.total).sub(new BN(balance)).toString();
    logger.info('Invoice will use customer credit', { result, remaining, total: props.total });
  }

  // get subscription items
  let subscriptionItems: SubscriptionItem[] = [];
  if (subscriptions && subscriptions.length > 0) {
    subscriptionItems = await SubscriptionItem.findAll({
      where: { subscription_id: { [Op.in]: subscriptions.map((s) => s.id) } },
    });
  } else if (subscription) {
    subscriptionItems = await SubscriptionItem.findAll({
      where: { subscription_id: subscription?.id },
    });
  }

  function getLineSetup(x: TLineItemExpanded) {
    const price = getSubscriptionItemPrice(x);

    if (price.type === 'recurring' && trialing) {
      return {
        price,
        amount: '0',
        // @ts-ignore
        description: trialing ? `${price.product.name} (trialing)` : price.product.name,
        period: {
          start: props.period_start,
          end: props.period_end,
        },
      };
    }

    const calculatedAmount =
      x.custom_amount ||
      new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString();

    return {
      price,
      amount: calculatedAmount,
      description: price.product.name,
      period: undefined,
    };
  }
  // process line items
  const processedItems = lineItems.map((x: TLineItemExpanded) => {
    const setup = getLineSetup(x);
    const { price } = setup;
    let { quantity } = x;
    if (price.type === 'recurring') {
      if (price.recurring?.usage_type === 'metered' && !metered) {
        quantity = 0;
      }
      if (trialing) {
        quantity = 0;
      }
    }

    return {
      price_id: price.id,
      amount: quantity > 0 ? setup.amount : '0',
      quantity,
      description: setup.description,
      period: setup.period,
      metadata: x.metadata || {},
      subscription_item_id: subscriptionItems.find((si) => si.price_id === price.id)?.id,
      // Discount fields from pre-calculated line items
      discountable: x.discountable || false,
      discount_amounts: x.discount_amounts || [],
      // Pass quote_id for dynamic pricing audit trail
      quote_id: (x as any).quote_id,
    };
  });

  return createInvoiceWithItems({
    customer,
    subscription,
    items: processedItems,
    livemode: props.livemode,
    status: props.status || 'open',
    billing_reason: props.billing_reason,
    description: props.description || '',
    statement_descriptor: props.statement_descriptor,

    currency_id: props.currency_id,
    payment_intent_id: props.payment_intent_id,
    checkout_session_id: props.checkout_session_id,
    default_payment_method_id: props.default_payment_method_id,

    period_start: props.period_start,
    period_end: props.period_end,
    auto_advance: props.auto_advance,

    total: props.total,
    subtotal: props.subtotal || props.total,
    amount_remaining: props.amount_remaining || remaining,
    amount_paid: props.amount_paid,
    amount_due: props.amount_due || remaining,
    subtotal_excluding_tax: props.subtotal || props.total || '0',
    starting_token_balance: result.starting,
    ending_token_balance: result.ending,

    footer: props.footer,
    custom_fields: props.custom_fields,
    payment_settings: props.payment_settings || subscription?.payment_settings,
    discounts: props.discounts,
    total_discount_amounts: props.total_discount_amounts || [],
    metadata: {
      ...props.metadata,
      starting_token_balance: result.starting,
      ending_token_balance: result.ending,
    },
  });
}

export async function cleanupInvoiceAndItems(invoiceId: string) {
  const invoice = await Invoice.findByPk(invoiceId);
  if (!invoice) {
    return;
  }
  if (invoice.isImmutable()) {
    return;
  }

  const removedItem = await InvoiceItem.destroy({ where: { invoice_id: invoiceId } });
  const removedInvoice = await Invoice.destroy({ where: { id: invoiceId } });
  logger.info('cleanup invoice and items', { invoiceId, removedItem, removedInvoice });
}

export async function ensureRechargeInvoice(
  invoiceProps: {
    total: string;
    description?: string;
    checkout_session_id?: string;
    currency_id: string;
    metadata?: any;
    payment_settings?: any;
    livemode?: boolean;
  },
  subscription: Subscription | null,
  paymentMethod: PaymentMethod,
  customer: Customer
) {
  try {
    const { livemode = true } = invoiceProps;
    const { invoice } = await createInvoiceWithItems({
      customer,
      subscription,
      currency_id: invoiceProps.currency_id,
      livemode: subscription?.livemode ?? livemode,
      period_start: dayjs().unix(),
      period_end: dayjs().unix(),
      status: 'paid',
      billing_reason: 'recharge',
      description: invoiceProps?.description || 'Add funds for subscription',
      total: invoiceProps.total || '0',
      amount_due: '0',
      amount_paid: invoiceProps.total || '0',
      amount_remaining: '0',
      default_payment_method_id: paymentMethod.id,
      checkout_session_id: invoiceProps?.checkout_session_id || '',
      metadata: invoiceProps.metadata || {},
      auto_advance: false,
      paid: true,
      paid_out_of_band: false,
      payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
    });

    logger.info('create recharge invoice success', {
      invoice,
      subscriptionId: subscription?.id,
      paymentMethod: paymentMethod.id,
    });
  } catch (error) {
    logger.error('ensureRechargeInvoice: create invoice failed', {
      error,
      subscriptionId: subscription?.id,
      paymentMethod: paymentMethod.id,
    });
    throw error;
  }
}

export async function ensureOverdraftProtectionInvoiceAndItems({
  customer,
  subscription,
  paymentIntent,
  props,
}: {
  customer: Customer;
  subscription: Subscription;
  paymentIntent: PaymentIntent;
  props: Partial<TInvoice> & { period_start: number; period_end: number };
}): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
  const { price, product } = await ensureOverdraftProtectionPrice(subscription.livemode);
  const invoicePrice = price?.currency_options?.find((x: any) => x.currency_id === paymentIntent?.currency_id);

  if (!subscription.overdraft_protection?.enabled) {
    throw new Error('Overdraft protection invoice creation skipped: overdraft protection is not enabled');
  }

  if (!invoicePrice) {
    throw new Error('Overdraft protection invoice price not found');
  }
  const currency = await PaymentCurrency.findByPk(invoicePrice.currency_id);
  if (!currency) {
    throw new Error('Overdraft protection invoice currency not found');
  }
  const paymentMethod = await PaymentMethod.findByPk(currency?.payment_method_id);
  if (!paymentMethod) {
    throw new Error('Overdraft protection invoice payment method not found');
  }

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

  const { unused } = await isSubscriptionOverdraftProtectionEnabled(subscription, paymentIntent?.currency_id);
  if (new BN(unused).lt(new BN(invoicePrice.unit_amount))) {
    throw new Error('Overdraft protection invoice creation skipped: insufficient overdraft protection funds');
  }

  const result = await createInvoiceWithItems({
    customer,
    subscription,
    currency_id: invoicePrice.currency_id,
    livemode: subscription.livemode,
    period_start: props.period_start || 0,
    period_end: props.period_end || 0,
    status: props.status,
    billing_reason: 'overdraft_protection',
    description: 'Overdraft protection',
    collection_method: 'send_invoice',
    default_payment_method_id: paymentMethod.id,
    total: invoicePrice.unit_amount || '0',
    items: [
      {
        price_id: price?.id || '',
        amount: invoicePrice.unit_amount || '0',
        quantity: 1,
        description: product?.name || '',
        period: {
          start: props.period_start || 0,
          end: props.period_end || 0,
        },
      },
    ],
    metadata: {
      ...(props.metadata || {}),
      payment_intent_id: paymentIntent.id,
    },
  });

  await Price.increment({ quantity_sold: 1 }, { where: { id: price?.id } });
  return result;
}

export async function ensureStakeInvoice(
  invoiceProps: {
    total: string;
    description?: string;
    checkout_session_id?: string;
    currency_id: string;
    billing_reason?: string;
    metadata?: any;
    payment_settings?: any;
  },
  subscription: Subscription,
  paymentMethod: PaymentMethod,
  customer: Customer,
  allSubscriptions?: Subscription[]
) {
  if (paymentMethod.type !== 'arcblock') {
    return;
  }

  try {
    const isMultiSubscription = allSubscriptions && allSubscriptions.length > 1;
    const metadata = {
      ...(invoiceProps.metadata || {}),
    };

    if (isMultiSubscription) {
      metadata.all_subscription_ids = allSubscriptions.map((s) => s.id);
      metadata.is_shared_stake = true;
      metadata.primary_subscription_id = subscription.id;
    }

    const { invoice } = await createInvoiceWithItems({
      customer,
      subscription,
      currency_id: invoiceProps.currency_id,
      livemode: subscription.livemode,
      period_start: dayjs().unix(),
      period_end: subscription.current_period_end,
      status: 'paid',
      billing_reason: invoiceProps?.billing_reason || 'stake',
      description: invoiceProps?.description || 'Stake for subscription',
      total: invoiceProps.total || '0',
      amount_due: '0',
      amount_paid: invoiceProps.total || '0',
      amount_remaining: '0',
      default_payment_method_id: paymentMethod.id,
      checkout_session_id: invoiceProps?.checkout_session_id || '',
      metadata,
      auto_advance: false,
      paid: true,
      paid_out_of_band: false,
      payment_settings: invoiceProps?.payment_settings || subscription?.payment_settings,
    });

    logger.info('create staking invoice success', {
      invoice,
      subscriptionId: subscription?.id,
      paymentMethod: paymentMethod.id,
    });
  } catch (error) {
    logger.error('ensureStake: create invoice failed', {
      error,
      subscriptionId: subscription?.id,
      paymentMethod: paymentMethod.id,
    });
    throw error;
  }
}

export async function getStakeInvoiceForSubscription(subscriptionId: string, currencyId: string) {
  const where: WhereOptions = {
    [Op.or]: [
      { subscription_id: subscriptionId },
      { 'metadata.all_subscription_ids': { [Op.contains]: [subscriptionId] } },
    ],
    billing_reason: 'stake',
    status: 'paid',
  };

  if (currencyId) {
    where.currency_id = currencyId;
  }
  try {
    const invoice = await Invoice.findOne({ where });
    return invoice;
  } catch (error) {
    logger.error('getStakeInvoiceForSubscription: find invoice failed', {
      error,
      subscriptionId,
      currencyId,
    });
    return null;
  }
}

// mark overdraft protection invoice as void after payment
export async function handleOverdraftProtectionInvoiceAfterPayment(invoice: Invoice) {
  try {
    if (['stake', 'overdraft_protection', 'stake_overdraft_protection'].includes(invoice.billing_reason)) {
      return;
    }
    const subscription = await Subscription.findByPk(invoice.subscription_id);
    if (!subscription) {
      logger.info('handle overdraft protection invoice skipped due to subscription not found', {
        invoiceId: invoice.id,
      });
      return;
    }
    if (!subscription.overdraft_protection?.enabled) {
      logger.info('handle overdraft protection invoice skipped due to overdraft protection not enabled', {
        subscriptionId: subscription.id,
      });
      return;
    }
    const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
    if (!paymentMethod || paymentMethod?.type !== 'arcblock') {
      logger.info('handle overdraft protection invoice skipped due to payment method not supported', {
        invoiceId: invoice.id,
        paymentMethodId: paymentMethod?.id,
      });
      return;
    }
    const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
    if (!paymentIntent) {
      logger.info('handle overdraft protection invoice skipped due to payment intent not found', {
        invoiceId: invoice.id,
      });
      return;
    }
    if (paymentIntent.status !== 'succeeded') {
      logger.info('handle overdraft protection invoice skipped due to payment intent not succeeded', {
        invoiceId: invoice.id,
      });
      return;
    }
    if (paymentIntent.payment_details?.arcblock?.type !== 'transfer') {
      logger.info('handle overdraft protection invoice skipped due to payment type not transfer', {
        invoiceId: invoice.id,
        paymentIntentId: paymentIntent.id,
      });
      return;
    }
    const overdraftProtectionInvoice = await Invoice.findOne({
      where: {
        subscription_id: subscription.id,
        billing_reason: 'overdraft_protection',
        'metadata.invoice_id': invoice.id,
        status: {
          [Op.notIn]: ['paid', 'void'],
        },
      },
    });
    if (!overdraftProtectionInvoice) {
      logger.info('handle overdraft protection invoice skipped due to overdraft protection invoice not found', {
        invoiceId: invoice.id,
      });
      return;
    }

    if (overdraftProtectionInvoice.payment_intent_id) {
      const overdraftProtectionPaymentIntent = await PaymentIntent.findOne({
        where: {
          id: overdraftProtectionInvoice.payment_intent_id,
        },
      });
      if (overdraftProtectionPaymentIntent) {
        await overdraftProtectionPaymentIntent.update({
          status: 'canceled',
          canceled_at: dayjs().unix(),
          cancellation_reason: 'void_invoice',
        });
      }
    }

    await overdraftProtectionInvoice.update({
      status: 'void',
      status_transitions: {
        ...(overdraftProtectionInvoice.status_transitions || {}),
        voided_at: dayjs().unix(),
      },
    });
  } catch (error) {
    logger.error('handle overdraft protection invoice failed', {
      error,
      invoiceId: invoice.id,
    });
  }
}

/**
 * retry uncollectible invoices
 * @param options
 */
export async function retryUncollectibleInvoices(options: {
  customerId?: string;
  subscriptionId?: string;
  invoiceId?: string;
  invoiceIds?: string[];
  currencyId?: string;
  billingReason?: string;
  statusList?: string[];
}) {
  const lockKey = `retry-uncollectible-${JSON.stringify(options)}`;

  const isLocked = await Lock.isLocked(lockKey);
  if (isLocked) {
    logger.warn('Retry uncollectible invoices already in progress', {
      lockKey,
      options,
    });
    throw new Error('Retry already in progress');
  }

  try {
    await Lock.acquire(lockKey, dayjs().add(5, 'minutes').unix());

    const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId, billingReason, statusList } = options;

    const invoiceStatusList = statusList || ['uncollectible'];

    const where: WhereOptions = {
      status: { [Op.in]: invoiceStatusList },
      payment_intent_id: { [Op.ne]: null },
    };

    if (customerId) {
      where.customer_id = customerId;
    }

    if (subscriptionId) {
      where.subscription_id = subscriptionId;
    }

    if (invoiceId) {
      where.id = invoiceId;
    }

    if (invoiceIds && invoiceIds.length > 0) {
      where.id = { [Op.in]: invoiceIds };
    }

    if (currencyId) {
      where.currency_id = currencyId;
    }

    if (billingReason) {
      where.billing_reason = billingReason;
    }

    const overdueInvoices = (await Invoice.findAll({
      where,
      include: [{ model: PaymentIntent, as: 'paymentIntent' }],
      attributes: ['id', 'payment_intent_id', 'subscription_id', 'customer_id', 'created_at', 'status', 'currency_id'],
      order: [['created_at', 'ASC']],
    })) as (Invoice & { paymentIntent?: PaymentIntent })[];

    const startTime = Date.now();
    logger.info('Found invoices to retry', {
      count: overdueInvoices.length,
      criteria: options,
      statusList: invoiceStatusList,
      invoiceIds: overdueInvoices.map((inv) => inv.id),
    });

    const results = {
      processed: overdueInvoices.length,
      successful: [] as string[],
      failed: [] as Array<{ id: string; reason: string }>,
    };

    const now = dayjs().unix();
    const BATCH_SIZE = 10;
    const BATCH_DELAY_SECONDS = 0.1;

    const settledResults = await Promise.allSettled(
      overdueInvoices.map(async (invoice, index) => {
        const { paymentIntent } = invoice;
        if (!paymentIntent) {
          throw new Error('No payment intent found');
        }

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

        const batchIndex = Math.floor(index / BATCH_SIZE);
        const runAt = now + batchIndex * BATCH_DELAY_SECONDS;

        await emitAsync(
          'payment.queued',
          paymentIntent.id,
          { paymentIntentId: paymentIntent.id, retryOnError: true, ignoreMaxRetryCheck: true },
          { sync: false, runAt }
        );

        return invoice;
      })
    );

    settledResults.forEach((result, index) => {
      const invoice = overdueInvoices[index];
      if (!invoice) {
        return;
      }
      if (result.status === 'fulfilled') {
        results.successful.push(invoice.id);
        logger.info('Successfully queued uncollectible invoice retry', {
          invoiceId: invoice.id,
          customerId: invoice.customer_id,
          paymentIntentId: invoice.payment_intent_id,
        });
      } else {
        const error = result.reason;
        const errorType = error.name || 'Unknown';
        const errorCode = error.code || 'UNKNOWN_ERROR';

        results.failed.push({
          id: invoice.id,
          reason: error.message || 'Unknown error',
        });

        logger.error('Failed to queue uncollectible invoice retry', {
          invoiceId: invoice.id,
          customerId: invoice.customer_id,
          subscriptionId: invoice.subscription_id,
          paymentIntentId: invoice.payment_intent_id,
          errorType,
          errorCode,
          error,
        });
      }
    });

    const processingTime = Date.now() - startTime;
    logger.info('Completed retrying invoices', {
      totalProcessed: results.processed,
      successful: results.successful.length,
      failed: results.failed.length,
      statusList: invoiceStatusList,
      processingTimeMs: processingTime,
    });

    return results;
  } finally {
    await Lock.release(lockKey);
    logger.info('Released retry uncollectible lock', { lockKey });
  }
}

/**
 * Migrate billing when a subscription's payment method changes.
 * Steps:
 * 1. Check if all subscription items are prepaid (licensed recurring).
 * 2. If so, check if there is an unpaid invoice for the current period with the old payment method.
 * 3. If such an invoice exists, void the old invoice and cancel its payment intent if present.
 * 4. Create a new invoice for the current period with the new payment method.
 * 5. If any step fails, throw an error and log details.
 *
 * @param subscription Subscription instance
 * @param oldCurrencyId Old payment currency ID
 * @param newCurrencyId New payment currency ID
 * @returns Migration result: { migrated, oldInvoice, newInvoice }
 */
export const migrateSubscriptionPaymentMethodInvoice = async (
  subscription: Subscription,
  oldCurrencyId: string,
  newCurrencyId: string
) => {
  // 1. Check if all subscription items are prepaid (licensed recurring)
  const subscriptionItems = await SubscriptionItem.findAll({
    where: { subscription_id: subscription.id },
    include: [{ model: Price, as: 'price' }],
  });

  const subscriptionItemsExpanded = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
  const isPrepaid = subscriptionItemsExpanded.every((item: TLineItemExpanded) => {
    const price = getSubscriptionItemPrice(item);
    return price.type === 'recurring' && price.recurring?.usage_type === 'licensed';
  });

  if (!isPrepaid) {
    logger.info('Skip billing migration for non-prepaid items', {
      subscriptionId: subscription.id,
    });
    return { migrated: false, reason: 'has_non_prepaid_items' };
  }

  // 2. Find unpaid invoice for the current period with the old payment method
  const currentPeriodInvoice = await Invoice.findOne({
    where: {
      subscription_id: subscription.id,
      billing_reason: 'subscription_cycle',
      status: ['open', 'uncollectible'],
      currency_id: oldCurrencyId,
      period_start: {
        [Op.gte]: subscription.current_period_start,
        [Op.lt]: subscription.current_period_end,
      },
    },
    order: [['created_at', 'DESC']],
  });

  let voidedInvoice: Invoice | null = null;

  if (!currentPeriodInvoice) {
    voidedInvoice = await Invoice.findOne({
      where: {
        subscription_id: subscription.id,
        billing_reason: 'subscription_cycle',
        status: 'void',
        currency_id: oldCurrencyId,
        period_start: {
          [Op.gte]: subscription.current_period_start,
          [Op.lt]: subscription.current_period_end,
        },
      },
      order: [['created_at', 'DESC']],
    });
    if (!voidedInvoice) {
      logger.info('Skip billing migration for no unpaid invoice', {
        subscriptionId: subscription.id,
        periodStart: subscription.current_period_start,
        periodEnd: subscription.current_period_end,
        oldCurrencyId,
      });
      return { migrated: false, reason: 'no_unpaid_invoice' };
    }
  }

  // 3. Get old and new payment method/currency
  const oldPaymentCurrency = await PaymentCurrency.findByPk(oldCurrencyId);
  if (!oldPaymentCurrency) {
    throw new Error(`Payment currency ${oldCurrencyId} not found`);
  }
  const oldPaymentMethod = await PaymentMethod.findByPk(oldPaymentCurrency.payment_method_id);
  if (!oldPaymentMethod) {
    throw new Error(`Payment method for currency ${oldCurrencyId} not found`);
  }

  const newPaymentCurrency = await PaymentCurrency.findByPk(newCurrencyId);
  if (!newPaymentCurrency) {
    throw new Error(`Payment currency ${newCurrencyId} not found`);
  }
  const newPaymentMethod = await PaymentMethod.findByPk(newPaymentCurrency.payment_method_id);
  if (!newPaymentMethod) {
    throw new Error(`Payment method for currency ${newCurrencyId} not found`);
  }

  // 4. Stripe payment method is not supported for migration
  if (newPaymentMethod.type === 'stripe') {
    logger.info('Skip billing migration for stripe payment method', {
      subscriptionId: subscription.id,
    });
    return { migrated: false, reason: 'stripe_payment_method' };
  }

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

    const cancelOldInvoice = async (invoice: Invoice) => {
      try {
        if (invoice.payment_intent_id) {
          const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
          if (paymentIntent && paymentIntent.status !== 'canceled') {
            await paymentIntent.update({
              status: 'canceled',
              canceled_at: dayjs().unix(),
              cancellation_reason: 'void_invoice',
            });
          }
        }

        if (oldPaymentMethod.type === 'stripe' && invoice.metadata?.stripe_id) {
          const client = oldPaymentMethod.getStripeClient();
          await client.invoices.voidInvoice(invoice.metadata.stripe_id);
        }

        await invoice.update({
          status: 'void',
          status_transitions: {
            ...(invoice.status_transitions || {}),
            voided_at: dayjs().unix(),
          },
        });

        logger.info('Successfully voided old invoice for payment method change', {
          subscriptionId: subscription.id,
          oldInvoice: invoice.id,
          oldCurrency: oldCurrencyId,
          newCurrency: newCurrencyId,
        });
      } catch (error) {
        logger.error('Failed to void old invoice', {
          subscription: subscription.id,
          invoiceId: invoice.id,
          error: error.message,
        });
        throw error;
      }
    };

    const createNewInvoice = async () => {
      const preInvoice = currentPeriodInvoice || voidedInvoice;
      if (!preInvoice) {
        throw new Error('No unpaid invoice found');
      }
      const metadata: Record<string, any> = {
        prev_invoice_id: preInvoice.id,
      };

      // For dynamic pricing items, we need to create new quotes with the new currency's exchange rate
      const dynamicItems = subscriptionItemsExpanded.filter(
        (item) => ((item as any)?.upsell_price || (item as any)?.price)?.pricing_type === 'dynamic'
      );

      let enrichedLineItems = subscriptionItemsExpanded;

      if (dynamicItems.length > 0 && newPaymentMethod.type !== 'stripe') {
        // Get fresh exchange rate for the new currency
        const exchangeRateService = getExchangeRateService();
        const quoteService = getQuoteService();
        const rateSymbol = getExchangeRateSymbol(newPaymentCurrency.symbol, newPaymentMethod.type as ChainType);
        const rateResult = await exchangeRateService.getRate(rateSymbol);

        logger.info('Creating new quotes for dynamic pricing items during payment method change', {
          subscriptionId: subscription.id,
          newCurrencyId,
          rateSymbol,
          exchangeRate: rateResult.rate,
          dynamicItemCount: dynamicItems.length,
        });

        // Create quotes for each dynamic pricing item
        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,
              target_currency_id: newCurrencyId,
              quantity: item.quantity,
              rateResult,
            });
            return { item, quoteResponse };
          })
        );

        // Build a map for quick lookup
        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);
        });

        // Enrich line items with quote information
        enrichedLineItems = subscriptionItemsExpanded.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,
            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;
        });

        // Store quote IDs in metadata for audit trail
        metadata.quote_ids = quoteResults.map((r) => r.quoteResponse.quote.id);
      }

      const amount = getSubscriptionCycleAmount(enrichedLineItems, newCurrencyId);

      const { invoice } = await ensureInvoiceAndItems({
        customer,
        currency: newPaymentCurrency,
        subscription,
        trialing: subscription.status === 'trialing',
        metered: false,
        lineItems: enrichedLineItems,
        applyCredit: false,
        props: {
          status: 'open',
          total: amount.total,
          livemode: subscription.livemode,
          description: 'Subscription cycle',
          statement_descriptor: preInvoice.statement_descriptor,
          period_start: preInvoice.period_start,
          period_end: preInvoice.period_end,
          auto_advance: true,
          billing_reason: 'subscription_cycle',
          currency_id: newCurrencyId,
          default_payment_method_id: newPaymentMethod.id,
          custom_fields: preInvoice.custom_fields || [],
          footer: preInvoice.footer || '',
          payment_settings: subscription.payment_settings,
          metadata,
        } as Invoice,
      });
      return invoice;
    };

    // 5. Cancel old invoice, then create new invoice
    if (currentPeriodInvoice) {
      await cancelOldInvoice(currentPeriodInvoice);
    }

    const invoice = await createNewInvoice();
    if (invoice) {
      await emitAsync('invoice.queued', invoice.id, { invoiceId: invoice.id, retryOnError: true }, { sync: false });
      logger.info('Successfully queued new invoice for payment method change', {
        subscriptionId: subscription.id,
        invoiceId: invoice.id,
      });
    }

    logger.info('Successfully migrated invoice for payment method change', {
      subscriptionId: subscription.id,
      oldInvoiceId: currentPeriodInvoice?.id || voidedInvoice?.id,
      oldCurrency: oldCurrencyId,
      newCurrency: newCurrencyId,
      newInvoiceId: invoice.id,
    });
    return {
      migrated: true,
      oldInvoice: currentPeriodInvoice,
      newInvoice: invoice,
    };
  } catch (error) {
    logger.error('Failed to migrate invoice for payment method change', {
      subscriptionId: subscription.id,
      oldCurrencyId,
      newCurrencyId,
      error: error.message,
    });
    throw error;
  }
};

export async function destroyExistingInvoice(invoice: Invoice) {
  const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
  try {
    if (paymentMethod?.type === 'stripe' && invoice.metadata?.stripe_id) {
      const client = paymentMethod.getStripeClient();
      await client.invoices.del(invoice.metadata.stripe_id);
    }
  } catch (error) {
    logger.error('Failed to destroy invoice on stripe', {
      error: error.message,
      invoiceId: invoice.id,
    });
  }
  await InvoiceItem.destroy({ where: { invoice_id: invoice.id } });
  await invoice.destroy();
}
