import { PaymentType } from "@prisma/client";
import Stripe from "stripe";
import { JsonValue } from "type-fest";
import { v4 as uuidv4 } from "uuid";

import { CalendarEvent } from "@lib/calendarClient";
import EventOrganizerRefundFailedMail from "@lib/emails/EventOrganizerRefundFailedMail";
import EventPaymentMail from "@lib/emails/EventPaymentMail";
import { getErrorFromUnknown } from "@lib/errors";
import prisma from "@lib/prisma";

import { createPaymentLink } from "./client";

export type PaymentData = Stripe.Response<Stripe.PaymentIntent> & {
  stripe_publishable_key: string;
  stripeAccount: string;
};

export type StripeData = Stripe.OAuthToken & {
  default_currency: string;
};

const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!;
const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!;
const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!;

const stripe = new Stripe(stripePrivateKey, {
  apiVersion: "2020-08-27",
});

export async function handlePayment(
  evt: CalendarEvent,
  selectedEventType: {
    price: number;
    currency: string;
  },
  stripeCredential: { key: JsonValue },
  booking: {
    user: { email: string | null; name: string | null; timeZone: string } | null;
    id: number;
    startTime: { toISOString: () => string };
    uid: string;
  }
) {
  const paymentFee = Math.round(
    selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
  );
  const { stripe_user_id, stripe_publishable_key } = stripeCredential.key as Stripe.OAuthToken;

  const params: Stripe.PaymentIntentCreateParams = {
    amount: selectedEventType.price,
    currency: selectedEventType.currency,
    payment_method_types: ["card"],
    application_fee_amount: paymentFee,
  };

  const paymentIntent = await stripe.paymentIntents.create(params, { stripeAccount: stripe_user_id });

  const payment = await prisma.payment.create({
    data: {
      type: PaymentType.STRIPE,
      uid: uuidv4(),
      bookingId: booking.id,
      amount: selectedEventType.price,
      fee: paymentFee,
      currency: selectedEventType.currency,
      success: false,
      refunded: false,
      data: Object.assign({}, paymentIntent, {
        stripe_publishable_key,
        stripeAccount: stripe_user_id,
      }) as PaymentData as unknown as JsonValue,
      externalId: paymentIntent.id,
    },
  });

  const mail = new EventPaymentMail(
    createPaymentLink({
      paymentUid: payment.uid,
      name: booking.user?.name,
      date: booking.startTime.toISOString(),
    }),
    evt
  );
  await mail.sendEmail();

  return payment;
}

export async function refund(
  booking: {
    id: number;
    uid: string;
    startTime: Date;
    payment: {
      id: number;
      success: boolean;
      refunded: boolean;
      externalId: string;
      data: JsonValue;
      type: PaymentType;
    }[];
  },
  calEvent: CalendarEvent
) {
  try {
    const payment = booking.payment.find((e) => e.success && !e.refunded);
    if (!payment) return;

    if (payment.type != PaymentType.STRIPE) {
      await handleRefundError({
        event: calEvent,
        reason: "cannot refund non Stripe payment",
        paymentId: "unknown",
      });
      return;
    }

    const refund = await stripe.refunds.create(
      {
        payment_intent: payment.externalId,
      },
      { stripeAccount: (payment.data as unknown as PaymentData)["stripeAccount"] }
    );

    if (!refund || refund.status === "failed") {
      await handleRefundError({
        event: calEvent,
        reason: refund?.failure_reason || "unknown",
        paymentId: payment.externalId,
      });
      return;
    }

    await prisma.payment.update({
      where: {
        id: payment.id,
      },
      data: {
        refunded: true,
      },
    });
  } catch (e) {
    const err = getErrorFromUnknown(e);
    console.error(err, "Refund failed");
    await handleRefundError({
      event: calEvent,
      reason: err.message || "unknown",
      paymentId: "unknown",
    });
  }
}

async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
  console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
  try {
    await new EventOrganizerRefundFailedMail(opts.event, opts.reason, opts.paymentId).sendEmail();
  } catch (e) {
    console.error("Error while sending refund error email", e);
  }
}

export default stripe;
