/* eslint-disable @typescript-eslint/naming-convention */
import { isValid } from '@arcblock/did';
import { Router } from 'express';
import Joi from 'joi';
import pick from 'lodash/pick';
import { Op } from 'sequelize';

import { BN, fromUnitToToken } from '@ocap/util';
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
import { ensureStripeCustomer, ensureStripeSetupIntentForInvoicePayment } from '../integrations/stripe/resource';
import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
import { authenticate } from '../libs/security';
import { expandLineItems } from '../libs/session';
import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../libs/util';
import dayjs from '../libs/dayjs';
import { Customer } from '../store/models/customer';
import { Invoice } from '../store/models/invoice';
import { InvoiceItem } from '../store/models/invoice-item';
import { PaymentCurrency } from '../store/models/payment-currency';
import { PaymentIntent } from '../store/models/payment-intent';
import { PaymentMethod } from '../store/models/payment-method';
import { Price } from '../store/models/price';
import { Product } from '../store/models/product';
import { Subscription } from '../store/models/subscription';
import { getReturnStakeInvoices, getStakingInvoices, retryUncollectibleInvoices } from '../libs/invoice';
import {
  CheckoutSession,
  PaymentLink,
  TInvoiceExpanded,
  Discount,
  Coupon,
  PromotionCode,
  CreditGrant,
  TaxRate,
  PriceQuote,
} from '../store/models';
import { mergePaginate, defaultTimeOrderBy, getCachedOrFetch, DataSource } from '../libs/pagination';
import logger from '../libs/logger';
import { returnOverdraftProtectionQueue, returnStakeQueue } from '../queues/subscription';
import { checkRemainingStake } from '../libs/subscription';
import { getExchangeRateService } from '../libs/exchange-rate';

// Simple format amount helper for backend
function formatAmountForDisplay(amount: string, decimal: number, symbol: string): string {
  const tokenValue = fromUnitToToken(amount, decimal);
  return `${parseFloat(tokenValue).toFixed(Math.min(decimal, 6))} ${symbol}`;
}

const router = Router();
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true, embed: true });
const authPortal = authenticate<Invoice>({
  component: true,
  roles: ['owner', 'admin'],
  record: {
    // @ts-ignore
    model: Invoice,
    field: 'customer_id',
  },
});

// Helper function: Get staking invoices by subscription ID with error handling
async function getStakingInvoicesById(subscriptionId: string) {
  try {
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) return [];
    return await getStakingInvoices(subscription);
  } catch (error) {
    logger.error('Failed to get staking invoices by ID', { error, subscriptionId });
    return [];
  }
}

// Helper function: Get return stake invoices by subscription ID with error handling
async function getReturnStakeInvoicesById(subscriptionId: string) {
  try {
    const subscription = await Subscription.findByPk(subscriptionId);
    if (!subscription) return [];
    return await getReturnStakeInvoices(subscription);
  } catch (error) {
    logger.error('Failed to get return stake invoices by ID', { error, subscriptionId });
    return [];
  }
}

// Helper function: Create data sources for recover-from subscriptions (with cache)
function createRecoverFromSubscriptionSources(
  recoverFromSubscriptionIds: string[],
  include_staking: boolean | undefined,
  include_return_staking: boolean | undefined
): DataSource<Invoice>[] {
  const sources: DataSource<Invoice>[] = [];

  for (const id of recoverFromSubscriptionIds) {
    if (include_staking) {
      sources.push({
        count: async () => {
          try {
            const invoices = await getCachedOrFetch(`staking:${id}`, () => getStakingInvoicesById(id));
            return invoices.length;
          } catch (error) {
            logger.error('Failed to count cached staking invoices', { error, subscriptionId: id });
            return 0;
          }
        },
        fetch: async (limit: number, offset: number = 0) => {
          try {
            const invoices = await getCachedOrFetch(`staking:${id}`, () => getStakingInvoicesById(id));
            return invoices.slice(offset, offset + limit);
          } catch (error) {
            logger.error('Failed to fetch cached staking invoices', { error, subscriptionId: id });
            return [];
          }
        },
        meta: {
          type: 'cached' as const,
          estimatedSize: 50,
          cacheable: true,
        },
      });
    }

    if (include_return_staking) {
      sources.push({
        count: async () => {
          try {
            const invoices = await getCachedOrFetch(`return-staking:${id}`, () => getReturnStakeInvoicesById(id));
            return invoices.length;
          } catch (error) {
            logger.error('Failed to count cached return stake invoices', { error, subscriptionId: id });
            return 0;
          }
        },
        fetch: async (limit: number, offset: number = 0) => {
          try {
            const invoices = await getCachedOrFetch(`return-staking:${id}`, () => getReturnStakeInvoicesById(id));
            return invoices.slice(offset, offset + limit);
          } catch (error) {
            logger.error('Failed to fetch cached return stake invoices', { error, subscriptionId: id });
            return [];
          }
        },
        meta: {
          type: 'cached' as const,
          estimatedSize: 50,
          cacheable: true,
        },
      });
    }
  }

  return sources;
}

const schema = createListParamSchema<{
  status?: string;
  customer_id?: string;
  customer_did?: string;
  subscription_id?: string;
  currency_id?: string;
  ignore_zero?: boolean;
  include_staking?: boolean;
  include_return_staking?: boolean;
  include_overdraft_protection?: boolean;
  include_recovered_from?: boolean;
  include_quote?: boolean;
  tax_rate_id?: string;
}>({
  status: Joi.string().empty(''),
  customer_id: Joi.string().empty(''),
  customer_did: Joi.string().empty(''),
  subscription_id: Joi.string().empty(''),
  currency_id: Joi.string().empty(''),
  ignore_zero: Joi.boolean().empty(false),
  include_staking: Joi.boolean().empty(false),
  include_return_staking: Joi.boolean().empty(false),
  include_overdraft_protection: Joi.boolean().default(true),
  include_recovered_from: Joi.boolean().empty(false),
  include_quote: Joi.boolean().empty(false),
  tax_rate_id: Joi.string().empty(''),
});

const buildQuoteMetadata = (quote: PriceQuote) => {
  const slippagePercent = quote.slippage_percent ?? (quote.metadata as any)?.slippage?.percent ?? null;
  const minAcceptableRate = quote.min_acceptable_rate ?? (quote.metadata as any)?.slippage?.min_acceptable_rate ?? null;
  const maxPayableToken = quote.max_payable_token ?? (quote.metadata as any)?.slippage?.max_payable_token ?? null;
  const derivedAtMs = quote.slippage_derived_at_ms ?? (quote.metadata as any)?.slippage?.derived_at_ms ?? null;
  const degraded = (quote.metadata as any)?.risk?.degraded ?? null;
  const degradedReason = (quote.metadata as any)?.risk?.degraded_reason ?? null;

  return {
    id: quote.id,
    base_currency: quote.base_currency,
    base_amount: quote.base_amount,
    quoted_amount: quote.quoted_amount,
    target_currency_id: quote.target_currency_id,
    rate_currency_symbol: quote.rate_currency_symbol,
    exchange_rate: quote.exchange_rate,
    rate_provider_name: quote.rate_provider_name,
    rate_provider_id: quote.rate_provider_id,
    rate_timestamp_ms: quote.rate_timestamp_ms,
    consensus_method: (quote.metadata as any)?.rate?.consensus_method || null,
    providers: (quote.metadata as any)?.rate?.providers || [],
    degraded,
    degraded_reason: degradedReason,
    slippage_percent: slippagePercent,
    min_acceptable_rate: minAcceptableRate,
    max_payable_token: maxPayableToken,
    derived_at_ms: derivedAtMs,
    status: quote.status,
  };
};

const attachQuoteMetadataToLines = (lines: any[] | undefined, quotesById: Map<string, PriceQuote>) => {
  if (!lines?.length) {
    return;
  }
  lines.forEach((line) => {
    const quoteId = line?.metadata?.quote_id;
    if (!quoteId) {
      return;
    }
    const quote = quotesById.get(quoteId);
    if (!quote) {
      return;
    }
    line.metadata = {
      ...(line.metadata || {}),
      quote: {
        ...(line.metadata?.quote || {}),
        ...buildQuoteMetadata(quote),
      },
    };
  });
};

router.get('/', authMine, async (req, res) => {
  try {
    // eslint-disable-next-line @typescript-eslint/naming-convention
    const {
      page,
      pageSize,
      livemode,
      status,
      ignore_zero,
      include_staking,
      include_return_staking,
      include_overdraft_protection = true,
      include_quote = false,
      ...query
    } = await schema.validateAsync(req.query, {
      stripUnknown: false,
      allowUnknown: true,
    });

    const taxRateId = query.tax_rate_id;
    delete query.tax_rate_id;
    delete (query as any).include_quote;
    const includeQuote = include_quote === true;

    const where = getWhereFromKvQuery(query.q);

    if (status) {
      where.status = status
        .split(',')
        .map((x) => x.trim())
        .filter(Boolean);
    }
    if (ignore_zero) {
      where.subtotal = { [Op.ne]: '0' };
    }
    if (query.customer_id) {
      where.customer_id = query.customer_id;
    }
    if (query.currency_id) {
      where.currency_id = query.currency_id;
    }
    if (query.customer_did && isValid(query.customer_did)) {
      const customer = await Customer.findOne({ where: { did: query.customer_did } });
      if (customer) {
        where.customer_id = customer.id;
      } else {
        res.json({ count: 0, list: [], paging: { page, pageSize } });
        return;
      }
    }
    if (query.subscription_id) {
      if (query.include_recovered_from) {
        where.subscription_id = [query.subscription_id].concat(
          await Subscription.getFromSubscriptions(query.subscription_id)
        );
      } else {
        where.subscription_id = query.subscription_id;
      }
    }
    if (typeof livemode === 'boolean') {
      where.livemode = livemode;
    }

    Object.keys(query)
      .filter((x) => x.startsWith('metadata.'))
      .forEach((key: string) => {
        // @ts-ignore
        where[key] = query[key];
      });

    const excludeBillingReasons = ['recharge'];
    if (!include_overdraft_protection) {
      excludeBillingReasons.push('stake_overdraft_protection');
    }
    if (!!(include_staking && query.subscription_id) || !include_staking) {
      excludeBillingReasons.push('stake');
    }
    if (excludeBillingReasons.length > 0) {
      where.billing_reason = { [Op.notIn]: excludeBillingReasons };
    }

    if (taxRateId) {
      const [count, list] = await Promise.all([
        Invoice.count({
          where,
          include: [
            {
              model: InvoiceItem,
              as: 'lines',
              where: { tax_rate_id: taxRateId },
              attributes: [],
              required: true,
            },
          ],
          distinct: true,
        }),
        Invoice.findAll({
          where,
          order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
          limit: pageSize,
          offset: (page - 1) * pageSize,
          subQuery: false,
          include: [
            {
              model: InvoiceItem,
              as: 'lines',
              where: { tax_rate_id: taxRateId },
              attributes: includeQuote ? ['id', 'metadata', 'amount', 'quantity', 'price_id'] : [],
              required: true,
            },
            { model: PaymentCurrency, as: 'paymentCurrency' },
            { model: PaymentMethod, as: 'paymentMethod' },
            { model: Subscription, as: 'subscription', attributes: ['id', 'description'] },
            { model: Customer, as: 'customer' },
          ],
        }).then(async (result) => {
          if (!includeQuote) {
            return result;
          }
          const quoteIds = new Set<string>();
          result.forEach((invoice) => {
            (invoice as any).lines?.forEach((line: any) => {
              const quoteId = line?.metadata?.quote_id;
              if (typeof quoteId === 'string' && quoteId.length > 0) {
                quoteIds.add(quoteId);
              }
            });
          });
          if (quoteIds.size === 0) {
            return result;
          }
          const quotes = await PriceQuote.findAll({ where: { id: Array.from(quoteIds) } });
          const quotesById = new Map(quotes.map((quote) => [quote.id, quote]));
          result.forEach((invoice) => {
            attachQuoteMetadataToLines((invoice as any).lines, quotesById);
          });
          return result;
        }),
      ]);

      res.json({
        count,
        list,
        paging: { page, pageSize },
      });
      return;
    }

    const sources: DataSource<Invoice>[] = [];

    // Primary data source: Invoice database records
    sources.push({
      count: () => {
        return Invoice.count({ where });
      },
      fetch: async (limit: number, offset: number = 0) => {
        const result = await Invoice.findAll({
          where,
          order: getOrder(req.query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
          limit,
          offset,
          include: [
            ...(includeQuote
              ? [
                  {
                    model: InvoiceItem,
                    as: 'lines',
                    attributes: ['id', 'metadata', 'amount', 'quantity', 'price_id'],
                  },
                ]
              : []),
            { model: PaymentCurrency, as: 'paymentCurrency' },
            { model: PaymentMethod, as: 'paymentMethod' },
            { model: Subscription, as: 'subscription', attributes: ['id', 'description'] },
            {
              model: InvoiceItem,
              as: 'lines',
              include: [
                {
                  model: Price,
                  as: 'price',
                  attributes: ['id', 'metadata'],
                  include: [{ model: Product, as: 'product', attributes: ['id', 'type', 'name'] }],
                },
              ],
            },
            { model: Customer, as: 'customer' },
          ],
        });
        if (includeQuote && result.length > 0) {
          const quoteIds = new Set<string>();
          result.forEach((invoice) => {
            (invoice as any).lines?.forEach((line: any) => {
              const quoteId = line?.metadata?.quote_id;
              if (typeof quoteId === 'string' && quoteId.length > 0) {
                quoteIds.add(quoteId);
              }
            });
          });
          if (quoteIds.size > 0) {
            const quotes = await PriceQuote.findAll({ where: { id: Array.from(quoteIds) } });
            const quotesById = new Map(quotes.map((quote) => [quote.id, quote]));
            result.forEach((invoice) => {
              attachQuoteMetadataToLines((invoice as any).lines, quotesById);
            });
          }
        }
        return result;
      },
      meta: {
        type: 'database' as const,
        cacheable: false,
      },
    });

    // Handle subscription_id array
    let subscriptionIds: string[] = [];
    if (Array.isArray(where.subscription_id)) {
      subscriptionIds = where.subscription_id;
    } else if (where.subscription_id) {
      subscriptionIds = [where.subscription_id];
    }

    // Process active subscription (no cache) - only if staking options are enabled
    const activeSubscriptionId = subscriptionIds[0];
    if (include_staking && activeSubscriptionId) {
      const subscription = await Subscription.findByPk(activeSubscriptionId);
      if (subscription?.payment_details?.arcblock?.staking?.tx_hash) {
        // Add staking invoices source
        if (include_staking) {
          sources.push({
            count: async () => {
              try {
                const invoices = await getStakingInvoices(subscription);
                return invoices.length;
              } catch (error) {
                logger.error('Failed to count staking invoices', { error, subscriptionId: activeSubscriptionId });
                return 0;
              }
            },
            fetch: async (limit: number, offset: number = 0) => {
              try {
                const invoices = await getStakingInvoices(subscription);
                return invoices.slice(offset, offset + limit);
              } catch (error) {
                logger.error('Failed to fetch staking invoices', { error, subscriptionId: activeSubscriptionId });
                return [];
              }
            },
            meta: {
              type: 'computed' as const,
              estimatedSize: 50, // Most staking invoices are small
              cacheable: true,
            },
          });
        }

        // Add return stake invoices source
        if (include_return_staking) {
          sources.push({
            count: async () => {
              try {
                const invoices = await getReturnStakeInvoices(subscription);
                return invoices.length;
              } catch (error) {
                logger.error('Failed to count return stake invoices', { error, subscriptionId: activeSubscriptionId });
                return 0;
              }
            },
            fetch: async (limit: number, offset: number = 0) => {
              try {
                const invoices = await getReturnStakeInvoices(subscription);
                return invoices.slice(offset, offset + limit);
              } catch (error) {
                logger.error('Failed to fetch return stake invoices', { error, subscriptionId: activeSubscriptionId });
                return [];
              }
            },
            meta: {
              type: 'computed' as const,
              estimatedSize: 50, // Return stake invoices are usually fewer
              cacheable: true,
            },
          });
        }
      }
    }

    // Process recover-from subscriptions (with cache) - only if staking options are enabled
    const recoverFromSubscriptionIds = subscriptionIds.slice(1);
    if (include_staking && recoverFromSubscriptionIds.length > 0) {
      const recoverFromSources = createRecoverFromSubscriptionSources(
        recoverFromSubscriptionIds,
        include_staking,
        include_return_staking
      );
      sources.push(...recoverFromSources);
    }

    // Use generic pagination with intelligent strategy
    const result = await mergePaginate(
      sources,
      { page: Number(page), pageSize: Number(pageSize) },
      defaultTimeOrderBy(query.o === 'asc' ? 'asc' : 'desc')
    );

    res.json({
      count: result.total,
      list: result.data,
      paging: result.paging,
    });
  } catch (err) {
    logger.error('Failed to fetch invoices', {
      error: err,
      page: req.query.page,
      pageSize: req.query.pageSize,
      include_staking: req.query.include_staking,
      subscription_id: req.query.subscription_id,
    });
    res.json({
      count: 0,
      list: [],
      paging: { page: Number(req.query.page || 1), pageSize: Number(req.query.pageSize || 10) },
    });
  }
});

const rechargeSchema = createListParamSchema<{
  status?: string;
  customer_id?: string;
  currency_id?: string;
  recharge_address?: string;
}>({
  status: Joi.string().empty(''),
  customer_id: Joi.string().empty(''),
  currency_id: Joi.string().empty(''),
  recharge_address: Joi.string().empty(''),
});

router.get('/recharge', authMine, async (req, res) => {
  const { page, pageSize, ...query } = await rechargeSchema.validateAsync(req.query, {
    stripUnknown: false,
    allowUnknown: true,
  });
  const where = getWhereFromKvQuery(query.q);
  if (query.customer_id) {
    const customer = await Customer.findByPkOrDid(query.customer_id);
    if (!customer) {
      return res.status(404).json({ error: 'Customer not found' });
    }
    where.customer_id = customer.id;
  }
  if (query.currency_id) {
    where.currency_id = query.currency_id;
  }

  if (query.recharge_address) {
    where['metadata.payment_details.receiverAddress'] = query.recharge_address;
  }
  try {
    const { rows: invoices, count } = await Invoice.findAndCountAll({
      where: {
        billing_reason: 'recharge',
        paid: true,
        ...where,
      },
      offset: (page - 1) * pageSize,
      limit: pageSize,
      order: getOrder(req.query, [['created_at', 'DESC']]),
      include: [
        { model: PaymentCurrency, as: 'paymentCurrency' },
        { model: PaymentMethod, as: 'paymentMethod' },
      ],
    });

    return res.json({ count, list: invoices, paging: { page, pageSize } });
  } catch (err) {
    logger.error(err);
    return res.status(400).json({ error: err.message });
  }
});

const searchSchema = createListParamSchema<{}>({});
router.get('/search', authMine, async (req, res) => {
  const { page, pageSize, livemode, q, o } = await searchSchema.validateAsync(req.query, {
    stripUnknown: false,
    allowUnknown: true,
  });

  const where = getWhereFromKvQuery(q);
  if (typeof livemode === 'boolean') {
    where.livemode = livemode;
  }

  const { rows: list, count } = await Invoice.findAndCountAll({
    where,
    order: getOrder(req.query, [['created_at', o === 'asc' ? 'ASC' : 'DESC']]),
    offset: (page - 1) * pageSize,
    limit: pageSize,
    distinct: true,
    include: [
      { model: PaymentCurrency, as: 'paymentCurrency' },
      { model: Customer, as: 'customer' },
    ],
  });

  res.json({ count, list, paging: { page, pageSize } });
});

const retryUncollectibleSchema = Joi.object({
  customerId: Joi.string().trim().allow('').optional(),
  subscriptionId: Joi.string().trim().allow('').optional(),
  invoiceId: Joi.string().trim().allow('').optional(),
  invoiceIds: Joi.alternatives()
    .try(
      Joi.array().items(Joi.string().trim()),
      Joi.string()
        .trim()
        .custom((value) => {
          if (!value) return undefined;
          return value
            .split(',')
            .map((id: string) => id.trim())
            .filter(Boolean);
        })
    )
    .optional(),
  currencyId: Joi.string().trim().allow('').optional(),
});
router.get('/retry-uncollectible', authAdmin, async (req, res) => {
  try {
    const { error, value } = retryUncollectibleSchema.validate(req.query, {
      stripUnknown: true,
    });

    if (error) {
      return res.status(400).json({ error: error.message });
    }

    const { customerId, subscriptionId, invoiceId, invoiceIds, currencyId } = value;

    const result = await retryUncollectibleInvoices({
      customerId,
      subscriptionId,
      invoiceId,
      invoiceIds,
      currencyId,
    });

    return res.json(result);
  } catch (error) {
    logger.error('Failed to retry uncollectible invoices', { error });
    return res.status(500).json({
      error: 'Failed to retry uncollectible invoices',
      message: error.message,
    });
  }
});

router.get('/:id/return-stake', authAdmin, async (req, res) => {
  const doc = await Invoice.findByPk(req.params.id as string);
  if (!doc) {
    return res.status(404).json({ error: 'Invoice not found' });
  }
  if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
    return res.status(400).json({ error: 'Invoice is not a stake invoice' });
  }
  const paymentCurrency = await PaymentCurrency.findByPk(doc.currency_id);
  if (!paymentCurrency) {
    return res.status(400).json({ error: 'Payment currency not found' });
  }
  const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
  if (!paymentMethod) {
    return res.status(400).json({ error: 'Payment method not found' });
  }
  const stakingAddress = doc.metadata?.payment_details?.arcblock?.address;
  if (!stakingAddress) {
    return res.status(400).json({ error: 'Staking address not found' });
  }
  const { staked } = await checkRemainingStake(paymentMethod, paymentCurrency, stakingAddress, '0');
  return res.json(staked);
});
router.post('/:id/return-stake', authAdmin, async (req, res) => {
  const doc = await Invoice.findByPk(req.params.id as string);
  if (!doc) {
    return res.status(404).json({ error: 'Invoice not found' });
  }
  if (!['stake', 'stake_overdraft_protection'].includes(doc.billing_reason)) {
    return res.status(400).json({ error: 'Invoice is not a stake invoice' });
  }
  if (doc.status !== 'paid') {
    return res.status(400).json({ error: 'Invoice is not paid' });
  }
  const paymentMethod = await PaymentMethod.findByPk(doc.default_payment_method_id);
  if (!paymentMethod) {
    return res.status(400).json({ error: 'Payment method not found' });
  }
  if (paymentMethod.type !== 'arcblock') {
    return res.status(400).json({ error: 'Can only return stake for arcblock payment method' });
  }
  const subscription = await Subscription.findByPk(doc.subscription_id);
  if (!subscription) {
    return res.status(400).json({ error: 'Subscription not found' });
  }
  try {
    if (doc.billing_reason === 'stake') {
      await returnStakeQueue.pushAndWait({
        id: `return-stake-${subscription.id}-${doc.id}`,
        job: {
          subscriptionId: subscription.id,
          stakingAddress: doc.metadata?.payment_details?.arcblock?.address,
          paymentCurrencyId: doc.currency_id,
        },
      });
      return res.json({ success: true, subscriptionId: subscription.id });
    }
    if (doc.billing_reason === 'stake_overdraft_protection' && subscription.status === 'canceled') {
      await returnOverdraftProtectionQueue.pushAndWait({
        id: `return-overdraft-protection-${subscription.id}`,
        job: { subscriptionId: subscription.id },
      });
      return res.json({ success: true, subscriptionId: subscription.id });
    }
    return res.json({ success: false, error: 'Subscription is not canceled' });
  } catch (error) {
    logger.error('Failed to return stake', { error, subscriptionId: subscription.id, invoiceId: doc.id });
    return res.status(400).json({ error: 'Failed to return stake' });
  }
});

router.get('/:id', authPortal, async (req, res) => {
  try {
    const doc = (await Invoice.findOne({
      where: { id: req.params.id },
      include: [
        { model: PaymentCurrency, as: 'paymentCurrency' },
        { model: PaymentMethod, as: 'paymentMethod' },
        { model: PaymentIntent, as: 'paymentIntent' },
        { model: Subscription, as: 'subscription' },
        {
          model: InvoiceItem,
          as: 'lines',
          include: [
            {
              model: TaxRate,
              as: 'tax_rate',
              attributes: ['id', 'display_name', 'percentage', 'country', 'state', 'postal_code'],
              required: false,
            },
          ],
        },
        { model: Customer, as: 'customer' },
      ],
    })) as TInvoiceExpanded | null;

    if (doc) {
      const shouldSync = req.query.sync === 'true' || !!req.query.forceSync;
      // Sync Stripe invoice when sync=true query parameter is present
      if (doc.metadata?.stripe_id && doc.status !== 'paid') {
        // @ts-ignore
        await syncStripeInvoice(doc, shouldSync);
      }
      if (doc.payment_intent_id) {
        const paymentIntent = await PaymentIntent.findByPk(doc.payment_intent_id);
        await syncStripePayment(paymentIntent!);
      }

      let checkoutSession = null;
      let paymentLink = null;
      if (doc.checkout_session_id) {
        try {
          checkoutSession = await CheckoutSession.findByPk(doc.checkout_session_id);
          if (checkoutSession?.payment_link_id) {
            paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
          }
        } catch (err) {
          console.error('Failed to get checkout session', err);
        }
      }
      // @ts-ignore
      const json = doc.toJSON();
      if (doc.paymentIntent?.beneficiaries) {
        const blockletJson = await getBlockletJson();
        let total = new BN('0');
        const promises = doc.paymentIntent.beneficiaries?.map((x: any) => {
          total = total.add(new BN(x.share));
          return getUserOrAppInfo(x.address, blockletJson);
        });
        const users = await Promise.all(promises);
        json.paymentIntent.beneficiaries = json.paymentIntent.beneficiaries.map((x: any, i: number) => {
          return {
            ...x,
            ...(users[i] || {}),
            percent: (parseFloat(new BN(x.share).mul(new BN('10000')).div(total).toString()) / 100).toFixed(1),
            amount: x.share,
          };
        });
      }
      // S1 optimization: Load only referenced prices (with product) instead of full table scans
      const priceIds: string[] = [
        ...new Set((json.lines || []).map((i: any) => i.price_id).filter(Boolean) as string[]),
      ];
      const pricesWithProduct =
        priceIds.length > 0
          ? await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] })
          : [];
      const pricesJson = pricesWithProduct.map((x: any) => x.toJSON());
      const productMap = new Map<string, any>();
      pricesJson.forEach((p: any) => {
        if (p.product) productMap.set(p.product.id, p.product);
      });
      const paymentCurrencies = (
        await PaymentCurrency.findAll({
          where: {
            type: 'credit',
          },
        })
      ).map((x) => x.toJSON());
      // @ts-ignore
      expandLineItems(json.lines, Array.from(productMap.values()), pricesJson, paymentCurrencies);

      // Get discount details from total_discount_amounts
      let discountDetails: any[] = [];
      if (doc.total_discount_amounts && doc.total_discount_amounts.length > 0) {
        try {
          // Extract discount IDs from total_discount_amounts
          const discountIds = doc.total_discount_amounts.map((item: any) => item.discount).filter((id: any) => id);

          if (discountIds.length > 0) {
            discountDetails = await Discount.findAll({
              where: {
                id: discountIds,
              },
              include: [
                {
                  model: Coupon,
                  as: 'coupon',
                },
                {
                  model: PromotionCode,
                  as: 'promotionCode',
                },
              ],
            });
          }
        } catch (error) {
          logger.error('Failed to fetch discount details from total_discount_amounts', {
            error,
            invoiceId: doc.id,
            total_discount_amounts: doc.total_discount_amounts,
          });
        }
      }

      let relatedCreditGrants: any[] = [];
      try {
        relatedCreditGrants = await CreditGrant.findAll({
          where: {
            customer_id: doc.customer_id,
            'metadata.invoice_id': doc.id,
          } as any,
          include: [
            {
              model: PaymentCurrency,
              as: 'paymentCurrency',
              attributes: ['id', 'symbol', 'decimal', 'name', 'type'],
            },
          ],
          order: [['created_at', 'DESC']],
        });
      } catch (error) {
        logger.error('Failed to fetch related credit grants', {
          error,
          invoiceId: doc.id,
          customerId: doc.customer_id,
        });
      }

      // Fetch related price quotes
      let quotes: any[] = [];
      try {
        quotes = await PriceQuote.findAll({
          where: {
            invoice_id: doc.id,
          },
          order: [['created_at', 'ASC']],
        });
        const quoteIds = new Set<string>();
        (json as any).lines?.forEach((line: any) => {
          const quoteId = line?.metadata?.quote_id;
          if (typeof quoteId === 'string' && quoteId.length > 0) {
            quoteIds.add(quoteId);
          }
        });
        if (quoteIds.size > 0) {
          const quotesById = new Map(quotes.map((quote) => [quote.id, quote]));
          const missingIds = Array.from(quoteIds).filter((id) => !quotesById.has(id));
          if (missingIds.length > 0) {
            const quotesByLine = await PriceQuote.findAll({ where: { id: missingIds } });
            quotesByLine.forEach((quote) => quotesById.set(quote.id, quote));
          }
          quotes = Array.from(quotesById.values());
          attachQuoteMetadataToLines((json as any).lines, quotesById);
        } else if (quotes.length > 0) {
          const quotesById = new Map(quotes.map((quote) => [quote.id, quote]));
          attachQuoteMetadataToLines((json as any).lines, quotesById);
        }
      } catch (error) {
        logger.error('Failed to fetch related price quotes', {
          error,
          invoiceId: doc.id,
        });
      }

      if (doc.metadata?.invoice_id || doc.metadata?.prev_invoice_id) {
        const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id || doc.metadata.prev_invoice_id, {
          attributes: ['id', 'number', 'status', 'billing_reason'],
        });
        return res.json({
          ...json,
          discountDetails,
          relatedInvoice,
          relatedCreditGrants,
          paymentLink,
          checkoutSession,
          quotes,
        });
      }
      return res.json({
        ...json,
        discountDetails,
        relatedCreditGrants,
        paymentLink,
        checkoutSession,
        quotes,
      });
    }
    return res.status(404).json(null);
  } catch (err) {
    logger.error(err);
    return res.status(500).json({ error: `Failed to get invoice: ${err.message}` });
  }
});

/**
 * Get available payment options for an invoice with estimated prices
 * Used when user wants to switch payment method after price change
 */
router.get('/:id/payment-options', authPortal, async (req, res) => {
  try {
    const invoice = await Invoice.findByPk(req.params.id, {
      include: [
        { model: InvoiceItem, as: 'lines' },
        { model: PaymentCurrency, as: 'paymentCurrency' },
      ],
    });

    if (!invoice) {
      return res.status(404).json({ error: 'Invoice not found' });
    }

    // Get all available currencies for this invoice's products
    const invoiceItems = (invoice as any).lines || [];
    const priceIds = invoiceItems.map((item: any) => item.price_id).filter(Boolean);

    // Get all unique product IDs from prices
    const prices = await Price.findAll({
      where: { id: priceIds },
      include: [{ model: PaymentCurrency, as: 'currencies' }],
    });

    // Collect all unique currencies
    const currencyMap = new Map<string, any>();
    const currentCurrencyId = invoice.currency_id;

    // Add current currency first
    const { paymentCurrency } = invoice as any;
    if (paymentCurrency) {
      const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
      currencyMap.set(paymentCurrency.id, {
        ...paymentCurrency.toJSON(),
        method: paymentMethod?.toJSON(),
      });
    }

    // Add other available currencies from prices
    for (const price of prices) {
      const priceCurrencies = (price as any).currencies || [];
      for (const currency of priceCurrencies) {
        if (!currencyMap.has(currency.id)) {
          // eslint-disable-next-line no-await-in-loop
          const paymentMethod = await PaymentMethod.findByPk(currency.payment_method_id);
          currencyMap.set(currency.id, {
            ...currency.toJSON(),
            method: paymentMethod?.toJSON(),
          });
        }
      }
    }

    // Calculate estimated amounts for each currency
    const exchangeRateService = getExchangeRateService();
    const options = [];

    for (const [currencyId, currency] of currencyMap) {
      let estimatedAmount = '';
      const isCurrentMethod = currencyId === currentCurrencyId;

      // For current method, use the invoice amount
      if (isCurrentMethod) {
        estimatedAmount = formatAmountForDisplay(invoice.amount_due, currency.decimal, currency.symbol);
      } else {
        // For other methods, calculate based on USD base amount and current exchange rate
        try {
          // Get USD total from line items
          let usdTotal = 0;
          for (const item of invoiceItems) {
            const itemPrice = prices.find((p) => p.id === item.price_id);
            if (itemPrice?.base_amount) {
              usdTotal += parseFloat(itemPrice.base_amount) * item.quantity;
            }
          }

          if (usdTotal > 0) {
            // Get exchange rate for this currency
            const rateSymbol =
              currency.method?.type === 'arcblock' ? 'ABT' : `${currency.symbol}@${currency.method?.type}`;
            // eslint-disable-next-line no-await-in-loop
            const rateResult = await exchangeRateService.getRate(rateSymbol);
            const rate = parseFloat(rateResult.rate);

            if (rate > 0) {
              const tokenAmount = usdTotal / rate;
              const tokenAmountInUnits = Math.floor(tokenAmount * 10 ** currency.decimal);
              estimatedAmount = `≈ ${formatAmountForDisplay(tokenAmountInUnits.toString(), currency.decimal, currency.symbol)}`;
            }
          }
        } catch (error) {
          logger.warn('Failed to calculate estimated amount for currency', {
            currencyId,
            error: (error as Error).message,
          });
          estimatedAmount = '—';
        }
      }

      options.push({
        currency,
        estimatedAmount,
        isCurrentMethod,
      });
    }

    // Sort: current method first, then by symbol
    options.sort((a, b) => {
      if (a.isCurrentMethod) return -1;
      if (b.isCurrentMethod) return 1;
      return a.currency.symbol.localeCompare(b.currency.symbol);
    });

    return res.json({ options });
  } catch (err: any) {
    logger.error('Failed to get payment options for invoice', { error: err, invoiceId: req.params.id });
    return res.status(500).json({ error: `Failed to get payment options: ${err.message}` });
  }
});

router.post('/pay-stripe', authPortal, async (req, res) => {
  try {
    const { invoice_ids, subscription_id, customer_id, currency_id } = req.body;

    if (!currency_id) {
      return res.status(400).json({ error: 'currency_id is required' });
    }

    if (!invoice_ids && !subscription_id && !customer_id) {
      return res.status(400).json({ error: 'Must provide invoice_ids, subscription_id, or customer_id' });
    }

    let invoices: Invoice[];
    let customer: Customer | null;
    let paymentMethod: PaymentMethod | null = null;

    if (invoice_ids && Array.isArray(invoice_ids) && invoice_ids.length > 0) {
      invoices = await Invoice.findAll({
        where: {
          id: { [Op.in]: invoice_ids },
          currency_id,
          status: { [Op.in]: ['open', 'uncollectible'] },
        },
        include: [
          { model: Customer, as: 'customer' },
          { model: PaymentCurrency, as: 'paymentCurrency' },
        ],
      });

      if (invoices.length === 0) {
        return res.status(404).json({ error: 'No payable invoices found' });
      }

      // @ts-ignore
      customer = invoices[0]?.customer;
      paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
    } else if (subscription_id) {
      const subscription = await Subscription.findByPk(subscription_id, {
        include: [{ model: Customer, as: 'customer' }],
      });

      if (!subscription) {
        return res.status(404).json({ error: 'Subscription not found' });
      }

      // @ts-ignore
      customer = subscription.customer;
      paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);

      invoices = await Invoice.findAll({
        where: {
          subscription_id,
          currency_id,
          status: { [Op.in]: ['open', 'uncollectible'] },
        },
        include: [
          { model: Customer, as: 'customer' },
          { model: PaymentCurrency, as: 'paymentCurrency' },
        ],
      });
    } else {
      customer = await Customer.findByPkOrDid(customer_id!);
      if (!customer) {
        return res.status(404).json({ error: 'Customer not found' });
      }

      invoices = await Invoice.findAll({
        where: {
          customer_id: customer.id,
          currency_id,
          status: { [Op.in]: ['open', 'uncollectible'] },
        },
        include: [
          { model: Customer, as: 'customer' },
          { model: PaymentCurrency, as: 'paymentCurrency' },
        ],
      });

      if (invoices.length === 0) {
        return res.status(404).json({ error: 'No payable invoices found' });
      }

      paymentMethod = await PaymentMethod.findByPk(invoices[0]!.default_payment_method_id);
    }

    if (!customer) {
      return res.status(404).json({ error: 'Customer not found' });
    }

    if (!paymentMethod || paymentMethod.type !== 'stripe') {
      return res.status(400).json({ error: 'Not using Stripe payment method' });
    }

    if (invoices.length === 0) {
      return res.status(400).json({ error: 'No payable invoices found' });
    }

    await ensureStripeCustomer(customer, paymentMethod);

    const settings = PaymentMethod.decryptSettings(paymentMethod.settings);

    const paymentCurrency = await PaymentCurrency.findByPk(currency_id);
    if (!paymentCurrency) {
      return res.status(404).json({ error: `Payment currency ${currency_id} not found` });
    }
    const totalAmount = invoices.reduce((sum, invoice) => {
      const amount = invoice.amount_remaining || '0';
      return new BN(sum).add(new BN(amount)).toString();
    }, '0');

    const metadata: any = {
      currency_id,
      customer_id: customer.id,
      invoices: JSON.stringify(invoices.map((inv) => inv.id)),
    };

    const setupIntent = await ensureStripeSetupIntentForInvoicePayment(customer, paymentMethod, metadata);

    return res.json({
      client_secret: setupIntent.client_secret,
      publishable_key: settings.stripe?.publishable_key,
      setup_intent_id: setupIntent.id,
      invoices: invoices.map((inv) => inv.id),
      amount: totalAmount,
      currency: paymentCurrency,
      customer,
    });
  } catch (err) {
    logger.error('Failed to create setup intent for stripe payment', {
      error: err,
      body: req.body,
    });
    return res.status(400).json({ error: err.message });
  }
});

// eslint-disable-next-line consistent-return
router.put('/:id', authAdmin, async (req, res) => {
  try {
    const doc = await Invoice.findByPk(req.params.id as string);
    if (!doc) {
      return res.status(404).json({ error: 'Invoice not found' });
    }

    const raw = pick(req.body, ['metadata']);
    if (raw.metadata) {
      const { error } = MetadataSchema.validate(raw.metadata);
      if (error) {
        return res.status(400).json({ error: error.message });
      }
      raw.metadata = formatMetadata(raw.metadata);
    }

    await doc.update(raw);
    res.json(doc);
  } catch (err) {
    logger.error(err);
    res.json(null);
  }
});

router.post('/:id/void', authAdmin, async (req, res) => {
  const invoice = await Invoice.findByPk(req.params.id as string);
  if (!invoice) {
    return res.status(404).json({ error: 'Invoice not found' });
  }
  if (['paid', 'void', 'draft'].includes(invoice.status)) {
    return res.status(400).json({ error: 'Can not void this invoice' });
  }
  const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
  if (!paymentMethod) {
    return res.status(400).json({ error: 'Payment method not found' });
  }
  if (invoice.subscription_id) {
    const subscription = await Subscription.findByPk(invoice.subscription_id);
    if (subscription && !subscription.isImmutable()) {
      return res.status(400).json({ error: 'Subscription is not immutable, can not void 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 (paymentMethod.type === 'stripe' && invoice.metadata?.stripe_id) {
      const client = paymentMethod.getStripeClient();
      await client.invoices.voidInvoice(invoice.metadata.stripe_id);
    }
    await invoice.update({
      status: 'void',
      status_transitions: {
        ...(invoice.status_transitions || {}),
        voided_at: dayjs().unix(),
      },
    });
    return res.json(invoice);
  } catch (error) {
    logger.error('Failed to void invoice', { error, invoiceId: invoice.id });
    return res.status(400).json({ error: 'Failed to void invoice' });
  }
});

export default router;
