import { Op } from 'sequelize';

import { getInvoiceShouldPayTotal, handleOverdraftProtectionInvoiceAfterPayment } from '../libs/invoice';
import { createEvent } from '../libs/audit';
import dayjs from '../libs/dayjs';
import logger from '../libs/logger';
import createQueue from '../libs/queue';
import { PaymentMethod, Invoice, InvoiceItem } from '../store/models';
import { CheckoutSession } from '../store/models/checkout-session';
import { PaymentIntent } from '../store/models/payment-intent';
import { Subscription } from '../store/models/subscription';
import { PriceQuote } from '../store/models/price-quote';
import { paymentQueue } from './payment';
import { getQuoteService } from '../libs/quote-service';

import { getLock } from '../libs/lock';
import { events } from '../libs/event';
import { batchHandleStripeInvoices } from '../integrations/stripe/resource';

type InvoiceJob = {
  invoiceId: string;
  justCreate?: boolean;
  retryOnError?: boolean;
  waitForPayment?: boolean;
};

// handle invoice payment
export const handleInvoice = async (job: InvoiceJob) => {
  logger.info('handle invoice', job);

  const invoice = await Invoice.findByPk(job.invoiceId);
  if (!invoice) {
    logger.warn('invoice not found', { invoiceId: job.invoiceId });
    return;
  }
  if (invoice.status !== 'open') {
    logger.warn('invoice not open', { invoiceId: job.invoiceId });
    return;
  }
  if (invoice.auto_advance === false) {
    logger.warn('invoice not configured to auto advance', { invoiceId: job.invoiceId });
    return;
  }

  const supportAutoCharge = await PaymentMethod.supportAutoCharge(invoice.default_payment_method_id);
  if (supportAutoCharge === false) {
    logger.warn('invoice does not support auto charge', { invoiceId: job.invoiceId });
    return;
  }

  // no payment required
  if (invoice.amount_remaining === '0') {
    logger.warn('invoice does not require payment', { invoiceId: job.invoiceId });

    await invoice.update({
      paid: true,
      status: 'paid',
      attempt_count: invoice.attempt_count + 1,
      attempted: true,
      status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
    });
    logger.info('Invoice updated to paid status', { invoiceId: invoice.id });

    if (invoice.subscription_id) {
      const subscription = await Subscription.findByPk(invoice.subscription_id);
      if (subscription) {
        if (subscription.status === 'incomplete') {
          await subscription.start();
          logger.info('invoice subscription updated', { invoice: invoice.id, subscription: subscription.id });
        } else if (subscription.status === 'trialing') {
          const started = await subscription.start();
          if (started) {
            logger.info('Subscription trialing ended on invoice done', {
              invoice: invoice.id,
              subscription: subscription.id,
            });
          }
        } else {
          if (invoice.billing_reason === 'subscription_cycle') {
            createEvent('Subscription', 'customer.subscription.renewed', subscription).catch(console.error);
          }
          if (invoice.billing_reason === 'subscription_update') {
            createEvent('Subscription', 'customer.subscription.upgraded', subscription).catch(console.error);
          }
        }
      }
    }

    if (invoice.checkout_session_id) {
      const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
      if (checkoutSession && checkoutSession.status === 'open') {
        await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
        logger.info('invoice checkout session updated', { invoice: invoice.id, checkoutSession: checkoutSession.id });
      }
    }

    if (invoice.payment_intent_id) {
      const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
      if (
        paymentIntent &&
        ['requires_action', 'requires_capture', 'requires_payment_method'].includes(paymentIntent.status)
      ) {
        await paymentIntent.update({
          amount_received: '0',
          status: 'succeeded',
        });
        logger.info('invoice payment intent updated', { invoice: invoice.id, paymentIntent: paymentIntent.id });
      }
    }

    return;
  }

  // charge automatically
  let paymentIntent: PaymentIntent | null = null;
  if (invoice.payment_intent_id) {
    logger.warn('PaymentIntent exist for invoice', {
      invoiceId: job.invoiceId,
      paymentIntent: invoice.payment_intent_id,
    });
    paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
    if (paymentIntent && paymentIntent.isImmutable() === false) {
      await paymentIntent.update({ status: 'requires_capture', customer_id: invoice.customer_id });
      logger.info('PaymentIntent updated for invoice', {
        invoiceId: invoice.id,
        paymentIntentId: paymentIntent.id,
        newStatus: 'requires_capture',
      });
    }
  } else {
    const descriptionMap: any = {
      subscription_create: 'Subscription creation',
      subscription_cycle: 'Subscription cycle',
      subscription_update: 'Subscription update',
      subscription_threshold: 'Subscription threshold',
      slash_stake: 'Slash stake',
      overdraft_protection: 'Overdraft protection',
    };
    // TODO: support partial payment from user balance
    paymentIntent = await PaymentIntent.create({
      livemode: !!invoice.livemode,
      amount: invoice.amount_remaining,
      amount_received: '0',
      amount_capturable: '0',
      customer_id: invoice.customer_id,
      description: descriptionMap[invoice.billing_reason] || '',
      currency_id: invoice.currency_id,
      payment_method_id: invoice.default_payment_method_id,
      invoice_id: invoice.id,
      status: 'requires_capture',
      capture_method: 'automatic',
      confirmation_method: 'automatic',
      payment_method_types: [],
      receipt_email: invoice.customer_email,
      statement_descriptor: invoice.statement_descriptor,
      statement_descriptor_suffix: '',
      setup_future_usage: 'on_session',
      metadata: {},
    });
    await invoice.update({ payment_intent_id: paymentIntent.id });
    logger.info('PaymentIntent created for invoice', {
      invoiceId: invoice.id,
      paymentIntentId: paymentIntent.id,
      amount: paymentIntent.amount,
    });

    if (invoice.checkout_session_id) {
      const checkoutSession = await CheckoutSession.findByPk(invoice.checkout_session_id);
      if (checkoutSession && checkoutSession.status === 'open') {
        await checkoutSession.update({ payment_intent_id: paymentIntent.id });
        logger.info('PaymentIntent attached to invoice checkout session', {
          invoice: invoice.id,
          checkoutSession: checkoutSession.id,
          paymentIntent: paymentIntent.id,
        });
      }
    }
  }
  if (paymentIntent) {
    if (job.justCreate) {
      return;
    }

    const existJob = await paymentQueue.get(paymentIntent.id);
    if (existJob) {
      logger.warn('Payment job already scheduled', { invoice: invoice.id, paymentIntent: paymentIntent.id });
      return;
    }
    logger.info('Payment job scheduled', { invoice: invoice.id, paymentIntent: paymentIntent.id });
    if (job.waitForPayment) {
      await paymentQueue.pushAndWait({
        id: paymentIntent.id,
        job: { paymentIntentId: paymentIntent.id, retryOnError: job.retryOnError },
      });
    } else {
      paymentQueue.push({
        id: paymentIntent.id,
        job: { paymentIntentId: paymentIntent.id, retryOnError: job.retryOnError },
      });
    }
  }
};

export const invoiceQueue = createQueue<InvoiceJob>({
  name: 'invoice',
  onJob: handleInvoice,
  options: {
    concurrency: 2,
    maxRetries: 3,
  },
});

export const startInvoiceQueue = async () => {
  const lock = getLock('startInvoiceQueue');
  if (lock.locked) {
    return;
  }

  try {
    await lock.acquire();
    const invoices = await Invoice.findAll({
      where: {
        status: 'open',
        collection_method: 'charge_automatically',
        amount_remaining: { [Op.gt]: '0' },
        billing_reason: { [Op.notIn]: ['stake', 'overdraft_protection'] },
      },
    });

    const results = await Promise.allSettled(
      invoices.map(async (x) => {
        const supportAutoCharge = await PaymentMethod.supportAutoCharge(x.default_payment_method_id);
        if (supportAutoCharge === false) {
          return;
        }
        const exist = await invoiceQueue.get(x.id);
        if (!exist) {
          await invoiceQueue.push({ id: x.id, job: { invoiceId: x.id, retryOnError: true } });
        }
      })
    );

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

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

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

events.on('invoice.paid', async ({ id: invoiceId }) => {
  const invoice = await Invoice.findByPk(invoiceId);
  if (!invoice) {
    logger.error('Invoice not found for paid event', { invoiceId });
    return;
  }
  logger.info('Processing paid invoice', { invoiceId, billingReason: invoice.billing_reason });

  const checkBillingReason = ['subscription_cycle', 'subscription_cancel'];
  if (checkBillingReason.includes(invoice.billing_reason)) {
    const shouldPayTotal = await getInvoiceShouldPayTotal(invoice);
    if (shouldPayTotal !== invoice.total) {
      createEvent('Invoice', 'billing.discrepancy', invoice);
      logger.warn('Billing discrepancy detected', {
        invoiceId,
        shouldPayTotal,
        invoiceTotal: invoice.total,
      });
    } else {
      logger.info('Invoice paid successfully with correct amount', { invoiceId, total: invoice.total });
    }
  }
  await handleOverdraftProtectionInvoiceAfterPayment(invoice);
});

// Separate listener to mark quotes as paid - independent of other invoice.paid handlers
events.on('invoice.paid', async ({ id: invoiceId }) => {
  try {
    const invoiceItems = await InvoiceItem.findAll({
      where: { invoice_id: invoiceId },
    });

    const quoteIds = invoiceItems
      .map((item) => item.metadata?.quote_id)
      .filter((id): id is string => typeof id === 'string' && id.length > 0);

    if (quoteIds.length === 0) {
      return;
    }

    const quoteService = getQuoteService();
    const markedQuoteIds: string[] = [];

    await Promise.all(
      quoteIds.map(async (quoteId) => {
        try {
          const quote = await PriceQuote.findByPk(quoteId);
          if (quote && quote.status !== 'paid') {
            await quote.update({ invoice_id: invoiceId });
            await quoteService.markAsPaid(quoteId);
            markedQuoteIds.push(quoteId);
          }
        } catch (err: any) {
          logger.warn('Failed to mark quote as paid', { quoteId, invoiceId, error: err.message });
        }
      })
    );

    if (markedQuoteIds.length > 0) {
      logger.info('Marked quotes as paid for invoice', {
        invoiceId,
        quoteIds: markedQuoteIds,
        totalQuotes: quoteIds.length,
      });
    }
  } catch (err: any) {
    logger.error('Failed to process quotes for paid invoice', { invoiceId, error: err.message });
  }
});

events.on('invoice.queued', async (id, job, args = {}) => {
  const { sync, ...extraArgs } = args;
  if (sync) {
    try {
      await invoiceQueue.pushAndWait({
        id,
        job,
        ...extraArgs,
      });
      events.emit('invoice.queued.done', { id });
    } catch (error) {
      logger.error('Error in invoice.queued', { id, job, error });
      events.emit('invoice.queued.error', { id, job, error });
    }
    return;
  }
  try {
    const existJob = await invoiceQueue.get(id);
    if (existJob) {
      await invoiceQueue.delete(id);
      logger.info('Removed existing invoice job for immediate execution', {
        id,
        originalRunAt: existJob.runAt,
      });
    }
    invoiceQueue.push({
      id,
      job,
      ...extraArgs,
    });
    events.emit('invoice.queued.done', { id });
  } catch (error) {
    logger.error('Error in invoice.queued', { id, job, error });
    events.emit('invoice.queued.error', { id, job, error });
  }
});
