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 dayjs from '../../../libs/dayjs';
import logger from '../../../libs/logger';
import { handlePaymentSucceed } from '../../../queues/payment';
import { Invoice, PaymentIntent, PaymentMethod, Subscription, TEventExpanded } from '../../../store/models';
import { handleStripeInvoiceCreated } from './invoice';
import { events } from '../../../libs/event';
import { handleSubscriptionOnPaymentFailure } from './subscription';

export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
  const triggerRenew = paymentIntent.status !== 'succeeded';
  await paymentIntent.update({
    status: 'succeeded',
    last_payment_error: null,
    amount_received: paymentIntent.amount,
    payment_details: merge(
      paymentIntent.metadata,
      event ? { stripe: { payment_intent_id: event.data.object.id, customer_id: event.data.object.customer } } : {}
    ),
  });
  logger.info('payment intent succeeded on stripe event', { locale: paymentIntent.id });

  const checkoutSessionId = event?.data?.object?.metadata?.checkoutSessionId;
  if (checkoutSessionId) {
    events.emit('checkout.session.pending_invoice', { checkoutSessionId, paymentIntentId: paymentIntent.id });
  }

  await handlePaymentSucceed(paymentIntent, triggerRenew);
}

export async function syncStripePayment(paymentIntent: PaymentIntent) {
  if (!paymentIntent?.metadata?.stripe_id) {
    return;
  }

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

  const triggerRenew = paymentIntent.status !== 'succeeded';
  const client = await method.getStripeClient();
  const stripeIntent = await client.paymentIntents.retrieve(paymentIntent.metadata.stripe_id);
  if (stripeIntent) {
    // @ts-ignore
    await paymentIntent.update({
      amount: String(stripeIntent.amount),
      amount_received: String(stripeIntent.amount_received),
      amount_capturable: String(stripeIntent.amount_capturable),
      amount_details: stripeIntent.amount_details as any,
      ...pick(stripeIntent, ['status', 'confirmation_method', 'capture_method', 'last_payment_error']),
    });
    logger.info('stripe payment intent synced', { locale: paymentIntent.id, remote: stripeIntent.id });

    if (stripeIntent.status === 'succeeded') {
      await handlePaymentSucceed(paymentIntent, triggerRenew);
    }
  }
}

export async function handleStripePaymentCreated(event: TEventExpanded, client: Stripe) {
  logger.info('possible payment intent from subscription', { id: event.id, type: event.type });

  const result = await handleStripeInvoiceCreated(event, client);
  if (!result) {
    return;
  }

  const { invoice, checkoutSession } = result;

  // create stripe intent
  const stripeIntent = event.data.object;
  let paymentIntent = await PaymentIntent.findOne({ where: { 'metadata.stripe_id': stripeIntent.id } });
  if (!paymentIntent) {
    // @ts-ignore
    paymentIntent = await PaymentIntent.create({
      customer_id: invoice.customer_id,
      currency_id: invoice.currency_id,
      payment_method_id: invoice.default_payment_method_id,
      invoice_id: invoice.id,
      payment_method_types: ['stripe'],

      amount: String(stripeIntent.amount),
      amount_received: String(stripeIntent.amount_received),
      amount_capturable: String(stripeIntent.amount_capturable),
      amount_details: stripeIntent.amount_details,
      setup_future_usage: stripeIntent.setup_future_usage || 'on_session',

      ...pick(stripeIntent, [
        'livemode',
        'description',
        'status',
        'confirmation_method',
        'capture_method',
        'receipt_email',
        'statement_descriptor',
        'statement_descriptor_suffix',
        'last_payment_error',
      ]),

      metadata: {
        stripe_id: stripeIntent.id,
      },
    });
    await client.paymentIntents.update(stripeIntent.id, { metadata: { appPid: env.appPid, id: paymentIntent.id } });
    await invoice.update({ payment_intent_id: paymentIntent.id });
    if (checkoutSession) {
      checkoutSession.update({ payment_intent_id: paymentIntent.id });
    }

    logger.info('stripe payment intent mirrored', { locale: paymentIntent.id, remote: stripeIntent.id });
    if (stripeIntent.status === 'succeeded') {
      await handlePaymentSucceed(paymentIntent);
    }
  }
}

const waitForStripePaymentMirrored = (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 handlePaymentIntentEvent(event: TEventExpanded, client: Stripe) {
  const localIntentId = event.data.object.metadata?.id;

  if (!localIntentId) {
    // We only handle payment_intents created from subscriptions
    if (event.data.object.invoice) {
      await handleStripePaymentCreated(event, client);
      return;
    }

    try {
      await waitForStripePaymentMirrored(event.data.object.id);
    } catch (err) {
      logger.error('wait for stripe payment intent mirror error', { id: event.id, type: event.type, error: err });
    }

    logger.warn('local payment intent id not found in strip event', { id: event.id, type: event.type });
    return;
  }

  const paymentIntent = await PaymentIntent.findByPk(localIntentId);
  if (!paymentIntent) {
    logger.warn('local payment intent not found', { localIntentId });
    return;
  }

  logger.info('received payment intent event', { id: event.id, type: event.type, localIntentId });

  if (event.type === 'payment_intent.succeeded') {
    await handleStripePaymentSucceed(paymentIntent, event);
    return;
  }

  if (event.type === 'payment_intent.canceled') {
    await paymentIntent.update({
      status: 'canceled',
      canceled_at: event.data.object.canceled_at,
      cancellation_reason: event.data.object.cancellation_reason,
    });
    logger.info('payment intent canceled on stripe event', { locale: paymentIntent.id });
    return;
  }

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

    if (paymentIntent.invoice_id) {
      const invoice = await Invoice.findByPk(paymentIntent.invoice_id);
      if (invoice) {
        await invoice.update({
          status: 'uncollectible',
          attempt_count: invoice.attempt_count + 1,
          attempted: true,
          status_transitions: { ...invoice.status_transitions, marked_uncollectible_at: dayjs().unix() },
        });

        if (invoice.subscription_id) {
          const subscription = await Subscription.findByPk(invoice.subscription_id);
          if (subscription) {
            handleSubscriptionOnPaymentFailure(subscription, event.type, client);
          }
        }
      }
    }
  }
}
