import { env } from '@blocklet/sdk/lib/env';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import pWaitFor from 'p-wait-for';
import type Stripe from 'stripe';

import type { WhereOptions } from 'sequelize';
import { checkUsageReportEmpty } from '../../../libs/subscription';
import { createEvent } from '../../../libs/audit';
import { getLock } from '../../../libs/lock';
import logger from '../../../libs/logger';
import {
  AutoRechargeConfig,
  CheckoutSession,
  Customer,
  Discount,
  Invoice,
  InvoiceItem,
  PaymentMethod,
  Subscription,
  SubscriptionItem,
  TEventExpanded,
  TInvoiceItem,
} from '../../../store/models';
import { handleSubscriptionOnPaymentFailure } from './subscription';

export async function handleStripeInvoicePaid(invoice: Invoice, event: TEventExpanded) {
  logger.info('invoice paid on stripe event', { locale: invoice.id });
  const processDiscounts = await processInvoiceDiscounts(
    event.data.object,
    invoice.subscription_id || '',
    invoice.checkout_session_id || ''
  );
  await invoice.update({
    status: 'paid',
    ...pick(event.data.object, [
      'amount_due',
      'amount_paid',
      'amount_remaining',
      'last_finalization_error',
      'paid_out_of_band',
      'paid',
      'status_transitions',
      'subtotal_excluding_tax',
      'subtotal',
      'tax',
      'total',
    ]),
    total_discount_amounts: processDiscounts,
  });
}

const processInvoiceDiscounts = async (stripeInvoice: any, subscriptionId: string, checkoutSessionId: string) => {
  const discountFilter: WhereOptions = {};
  if (checkoutSessionId) {
    discountFilter.checkout_session_id = checkoutSessionId;
  }
  if (subscriptionId) {
    discountFilter.subscription_id = subscriptionId;
  }
  const discount =
    stripeInvoice.total_discount_amounts && Object.keys(discountFilter).length > 0
      ? await Discount.findOne({
          where: discountFilter,
        })
      : null;
  const processDiscounts = stripeInvoice.total_discount_amounts
    ? stripeInvoice.total_discount_amounts.map((d: any) => {
        return {
          stripeDiscount: d.discount,
          discount: discount?.id || d.discount,
          amount: d.amount,
        };
      })
    : null;
  return processDiscounts;
};

export function getStripeInvoicePeriod(invoice: any) {
  const lineItem: TInvoiceItem = (invoice.lines.data || []).find((x: any) => !x.proration && x.type === 'subscription');
  if (lineItem && lineItem.period) {
    return {
      period_start: lineItem.period.start,
      period_end: lineItem.period.end,
    };
  }

  return {
    period_start: invoice.period_start,
    period_end: invoice.period_end,
  };
}

export async function syncStripeInvoice(invoice: Invoice, sync = true) {
  if (!invoice.metadata?.stripe_id) {
    return;
  }

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

  const client = await method.getStripeClient();
  const stripeInvoice = await client.invoices.retrieve(invoice.metadata.stripe_id);
  const updates = [
    'amount_due',
    'amount_paid',
    'amount_remaining',
    'last_finalization_error',
    'paid_out_of_band',
    'paid',
    'status',
    'status_transitions',
    'subtotal_excluding_tax',
    'subtotal',
    'tax',
    'total_discount_amounts',
    'total',
  ];
  if (!sync && stripeInvoice.status !== 'paid' && invoice.status === 'uncollectible') {
    // remove status from updates
    const statusIndex = updates.indexOf('status');
    if (statusIndex !== -1) {
      updates.splice(statusIndex, 1);
    }
  }
  if (stripeInvoice) {
    const processDiscounts = await processInvoiceDiscounts(
      stripeInvoice,
      invoice.subscription_id || '',
      invoice.checkout_session_id || ''
    );
    await invoice.update(
      // @ts-ignore
      merge(pick(stripeInvoice, updates), getStripeInvoicePeriod(stripeInvoice), {
        total_discount_amounts: processDiscounts,
      })
    );
    logger.info('stripe invoice synced', { locale: invoice.id, remote: stripeInvoice.id });
    const failedStatuses = ['uncollectible', 'finalization_failed', 'payment_failed'];
    if (failedStatuses.includes(stripeInvoice.status || '')) {
      logger.info('Invoice failed', { invoiceId: invoice.id });
      if (invoice.subscription_id) {
        const subscription = await Subscription.findByPk(invoice.subscription_id);
        if (subscription) {
          handleSubscriptionOnPaymentFailure(subscription, 'syncStripeInvoice', client);
        }
      }
    }
  }
}

export async function ensureStripeInvoice(stripeInvoice: any, subscription: Subscription, client: Stripe) {
  const lock = getLock(`mirror-stripe-invoice-${stripeInvoice.id}`);
  await lock.acquire();

  const customer = await Customer.findByPk(subscription.customer_id);
  if (!customer) {
    throw new Error('Customer not found');
  }
  const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);

  let invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
  if (invoice) {
    lock.release();
    return invoice;
  }

  const processTotalDiscounts = await processInvoiceDiscounts(
    stripeInvoice,
    subscription.id,
    checkoutSession?.id || ''
  );

  let processDiscounts = stripeInvoice.discounts;
  if (stripeInvoice.discounts && stripeInvoice.discounts.length > 0) {
    if (processTotalDiscounts) {
      processDiscounts = processTotalDiscounts.map((d: any) => d?.discount);
    }
  }

  const invoiceNumber = await customer.getInvoiceNumber();
  try {
    // @ts-ignore
    invoice = await Invoice.create({
      number: invoiceNumber,
      ...pick(stripeInvoice, [
        'amount_due',
        'amount_paid',
        'amount_remaining',
        'amount_shipping',
        'attempt_count',
        'attempted',
        'auto_advance',
        'billing_reason',
        'collection_method',
        'custom_fields',
        'customer_address',
        'customer_email',
        'customer_name',
        'customer_phone',
        'description',
        'discounts',
        'due_date',
        'effective_at',
        'ending_balance',
        'livemode',
        'paid_out_of_band',
        'paid',
        'period_end',
        'period_start',
        'starting_balance',
        'status_transitions',
        'status',
        'subtotal_excluding_tax',
        'subtotal',
        'tax',
        'total',
        'last_finalization_error',
      ]),
      discounts: processDiscounts,
      total_discount_amounts: processTotalDiscounts,
      currency_id: subscription.currency_id,
      customer_id: subscription.customer_id,
      default_payment_method_id: subscription.default_payment_method_id as string,
      payment_intent_id: '',
      subscription_id: subscription.id,
      checkout_session_id: checkoutSession?.id,
      statement_descriptor: stripeInvoice.statement_descriptor || '',

      payment_settings: subscription.payment_settings,
      metadata: {
        stripe_id: stripeInvoice.id,
        stripe_discounts: stripeInvoice.discounts,
      },
    });
  } catch (err: any) {
    // DB-level safety net: unique constraint on metadata.stripe_id prevents duplicate mirroring
    // even when the in-memory lock fails (CF Workers multi-isolate race condition)
    if (err.name === 'SequelizeUniqueConstraintError' || err.message?.includes('UNIQUE constraint failed')) {
      logger.warn('Duplicate stripe invoice creation caught by unique constraint', {
        stripeInvoiceId: stripeInvoice.id,
        subscriptionId: subscription.id,
      });
      invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoice.id } });
      if (invoice) {
        lock.release();
        return invoice;
      }
    }
    throw err;
  }
  if (checkoutSession) {
    await checkoutSession.update({ invoice_id: invoice.id });
  }
  if (subscription) {
    await subscription.update({ latest_invoice_id: invoice.id });
  }
  await client.invoices.update(stripeInvoice.id, { metadata: { appPid: env.appPid, id: invoice.id } });

  logger.info('stripe invoice mirrored', { local: invoice.id, remote: stripeInvoice.id });

  await Promise.all(
    stripeInvoice.lines.data.map(async (line: any) => {
      const subscriptionItem = line.subscription_item
        ? await SubscriptionItem.findOne({ where: { 'metadata.stripe_id': line.subscription_item } })
        : null;

      // @ts-ignore
      const item = await InvoiceItem.create({
        currency_id: subscription.currency_id,
        customer_id: subscription.customer_id,
        price_id: line.price?.metadata.id as string,
        invoice_id: invoice?.id as string,
        subscription_id: subscription.id,
        subscription_item_id: subscriptionItem?.id,
        ...pick(line, [
          'livemode',
          'amount',
          'quantity',
          'description',
          'period',
          'discountable',
          'discount_amounts',
          'discounts',
          'proration',
          'proration_details',
        ]),
        metadata: {
          stripe_id: line.id,
        },
      });

      logger.info('stripe invoice items mirrored', {
        local: item.id,
        remote: line.id,
      });
      return item;
    })
  );

  lock.release();
  return invoice;
}

export async function handleStripeInvoiceCreated(event: TEventExpanded, client: Stripe) {
  if (['invoice.created', 'payment_intent.created', 'payment_intent.succeeded'].includes(event.type) === false) {
    logger.warn('abort because event type not expected', { id: event.id, type: event.type });
    return null;
  }

  const stripeInvoiceId = event.type === 'invoice.created' ? event.data.object.id : event.data.object.invoice;
  const stripeInvoice = await client.invoices.retrieve(stripeInvoiceId);
  if (!stripeInvoice) {
    logger.warn('abort because stripe invoice not found', { id: event.id, type: event.type });
    return null;
  }
  if (!stripeInvoice.subscription) {
    logger.warn('abort because invoice have no subscription', { id: event.id, type: event.type });
    return null;
  }

  const stripeSubscription = await client.subscriptions.retrieve(stripeInvoice.subscription as string);
  if (!stripeSubscription) {
    logger.warn('abort because subscription not found', { id: event.id, type: event.type });
    return null;
  }
  if (stripeSubscription.metadata?.appPid !== env.appPid) {
    logger.warn('abort because subscription not interested', { id: event.id, type: event.type });
    return null;
  }

  const subscription = await Subscription.findByPk(stripeSubscription.metadata.id);
  if (!subscription) {
    logger.warn('abort because local subscription not exist', { id: event.id, type: event.type });
    return null;
  }
  const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
  if (!method || method.type !== 'stripe') {
    return null;
  }

  logger.info('valid event for subscription detected', {
    id: event.id,
    type: event.type,
    stripeInvoiceId: stripeInvoice.id,
    stripeSubscriptionId: stripeSubscription.id,
    localSubscriptionId: subscription.id,
  });

  const customer = await Customer.findByPk(subscription.customer_id);
  const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);

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

  // create stripe invoice
  const invoice = await ensureStripeInvoice(stripeInvoice, subscription, client);

  return { subscription, invoice, customer, checkoutSession };
}

const waitForStripeInvoiceMirrored = (stripeInvoiceId: string) => {
  return pWaitFor(
    async () => {
      const invoice = await Invoice.findOne({ where: { 'metadata.stripe_id': stripeInvoiceId } });
      return !!invoice;
    },
    { interval: 1000, timeout: 20 * 1000 }
  );
};

export async function handleInvoiceEvent(event: TEventExpanded, client: Stripe) {
  if (event.type === 'invoice.created') {
    await handleStripeInvoiceCreated(event, client);
    return;
  }

  let localInvoiceId = event.data.object.metadata?.id;

  // in case we missed some of the events
  const subscriptionId = event.data.object.subscription_details?.metadata?.id;
  const subscriptionAppPid = event.data.object.subscription_details?.metadata?.appPid;
  const autoRechargeConfigId = event.data.object.metadata?.recharge_id;

  if (!localInvoiceId) {
    // Handle subscription invoices
    if (subscriptionId && subscriptionAppPid && subscriptionAppPid === env.appPid) {
      logger.warn('try mirror invoice from stripe', { invoiceId: event.data.object.id });
      const subscription = await Subscription.findByPk(subscriptionId);
      if (subscription) {
        const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
        if (method && method.type === 'stripe') {
          const invoice = await ensureStripeInvoice(event.data.object, subscription, method.getStripeClient());
          localInvoiceId = invoice.id;
        }
      }
    } else {
      try {
        await waitForStripeInvoiceMirrored(event.data.object.id);
      } catch (err) {
        logger.error('wait for stripe invoice mirror error', { id: event.id, type: event.type, error: err });
      }

      logger.warn('local invoice id not found in stripe event', { id: event.id, type: event.type });
      return;
    }
  }

  const invoice = await Invoice.findByPk(localInvoiceId);
  if (!invoice) {
    logger.warn('local invoice not found', { localInvoiceId });
    return;
  }

  logger.info('received invoice event', { id: event.id, type: event.type, localInvoiceId });

  if (event.type === 'invoice.paid') {
    await handleStripeInvoicePaid(invoice, event);
    return;
  }

  if (event.type === 'invoice.finalized') {
    // invoice from draft to open
    await invoice.update({ status: 'open', status_transitions: event.data.object.status_transitions });
    logger.info('invoice finalized on stripe event', { locale: invoice.id });
    return;
  }

  if (event.type === 'invoice.voided') {
    await invoice.update({ status: 'void', status_transitions: event.data.object.status_transitions });
    logger.info('invoice voided on stripe event', { locale: invoice.id });
    return;
  }

  if (event.type === 'invoice.marked_uncollectible') {
    await invoice.update({
      status: 'uncollectible',
      status_transitions: event.data.object.status_transitions,
    });
    logger.info('invoice uncollectible on stripe event', { locale: invoice.id });
    return;
  }

  if (event.type === 'invoice.upcoming') {
    logger.info('received invoice upcoming event', event);
  }

  if (event.type === 'invoice.finalization_failed') {
    await invoice.update({
      status: 'uncollectible',
      last_finalization_error: event.data.object.last_finalization_error,
    });
    logger.info('invoice finalization failed on stripe event', { locale: invoice.id });
  }

  if (event.type === 'invoice.payment_failed') {
    await invoice.update({ status: 'uncollectible' });
    logger.info('invoice payment failed on stripe event', { locale: invoice.id });
  }

  const failedEvents = ['invoice.marked_uncollectible', 'invoice.finalization_failed', 'invoice.payment_failed'];
  if (failedEvents.includes(event.type)) {
    logger.info('invoice finalized and failed', { invoiceId: invoice.id });
    // Handle subscription payment failures
    if (invoice.subscription_id) {
      const subscription = await Subscription.findByPk(invoice.subscription_id);
      if (subscription) {
        handleSubscriptionOnPaymentFailure(subscription, event.type, client);
      }
    }
    if (autoRechargeConfigId && invoice) {
      const autoRechargeConfig = await AutoRechargeConfig.findByPk(autoRechargeConfigId);
      if (autoRechargeConfig) {
        autoRechargeConfig.update({
          enabled: false,
          metadata: {
            ...autoRechargeConfig.metadata,
            failReason: `Payment failed: ${event.type}`,
          },
        });
      }
    }
  }
}
