import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { db } from "~/server/db";
import { stripe } from "~/server/stripe";
import { users } from "~/server/db/schema/auth";
import { subscriptions } from "~/server/db/schema/subscription";
import type Stripe from "stripe";

type Tier = "basic" | "plus" | "premium";

export async function getOrCreateCustomer(userId: string, email: string) {
  const [user] = await db
    .select()
    .from(users)
    .where(eq(users.id, userId))
    .limit(1);

  if (!user) {
    throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
  }

  if (user.stripeCustomerId) {
    return user.stripeCustomerId;
  }

  const customer = await stripe.customers.create({
    email,
    metadata: { userId },
  });

  await db
    .update(users)
    .set({ stripeCustomerId: customer.id })
    .where(eq(users.id, userId));

  return customer.id;
}

export async function createCheckoutSession(
  userId: string,
  email: string,
  priceId: string,
  successUrl: string,
  cancelUrl: string,
) {
  const customerId = await getOrCreateCustomer(userId, email);

  const session = await stripe.checkout.sessions.create({
    customer: customerId,
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: successUrl,
    cancel_url: cancelUrl,
    metadata: { userId },
  });

  return { url: session.url, sessionId: session.id };
}

export async function createPortalSession(customerId: string, returnUrl: string) {
  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: returnUrl,
  });

  return { url: session.url };
}

export async function cancelSubscription(stripeSubscriptionId: string) {
  await stripe.subscriptions.update(stripeSubscriptionId, {
    cancel_at_period_end: true,
  });

  await db
    .update(subscriptions)
    .set({ cancelAtPeriodEnd: true })
    .where(eq(subscriptions.stripeId, stripeSubscriptionId));

  return { cancelAtPeriodEnd: true };
}

export async function reactivateSubscription(stripeSubscriptionId: string) {
  await stripe.subscriptions.update(stripeSubscriptionId, {
    cancel_at_period_end: false,
  });

  await db
    .update(subscriptions)
    .set({ cancelAtPeriodEnd: false })
    .where(eq(subscriptions.stripeId, stripeSubscriptionId));

  return { cancelAtPeriodEnd: false };
}

export async function listInvoices(
  customerId: string,
  limit: number = 10,
  startingAfter?: string,
) {
  const params: Stripe.InvoiceListParams = {
    customer: customerId,
    limit,
  };
  if (startingAfter) {
    params.starting_after = startingAfter;
  }

  const invoices = await stripe.invoices.list(params);
  return {
    invoices: invoices.data,
    hasMore: invoices.has_more,
  };
}

export async function updateSubscriptionInDB(
  stripeId: string,
  data: {
    tier?: Tier;
    status?: string;
    currentPeriodStart?: Date;
    currentPeriodEnd?: Date;
    cancelAtPeriodEnd?: boolean;
  },
) {
  const [existing] = await db
    .select()
    .from(subscriptions)
    .where(eq(subscriptions.stripeId, stripeId))
    .limit(1);

  if (existing) {
    const [updated] = await db
      .update(subscriptions)
      .set(data as Record<string, unknown>)
      .where(eq(subscriptions.stripeId, stripeId))
      .returning();
    return updated;
  }

  return null;
}

export async function handleWebhookEvent(event: Stripe.Event) {
  const obj = event.data.object as unknown as Record<string, unknown>;

  switch (event.type) {
    case "checkout.session.completed": {
      const session = obj as unknown as Stripe.Checkout.Session;
      const userId = session.metadata?.userId;
      if (!userId || !session.subscription) break;

      const stripeSub = await stripe.subscriptions.retrieve(
        session.subscription as string,
      );
      const sub = stripeSub as unknown as Record<string, unknown>;

      await db.insert(subscriptions).values({
        userId,
        stripeId: stripeSub.id,
        tier: mapStripeProductToTier(
          stripeSub.items.data[0]?.price?.id ?? "",
        ),
        status: sub.status as typeof subscriptions.$inferSelect.status,
        currentPeriodStart: new Date((sub.current_period_start as number) * 1000),
        currentPeriodEnd: new Date((sub.current_period_end as number) * 1000),
        cancelAtPeriodEnd: sub.cancel_at_period_end as boolean,
      }).onConflictDoNothing();
      break;
    }

    case "invoice.paid": {
      const invoice = obj;
      if (!invoice.subscription) break;

      await updateSubscriptionInDB(invoice.subscription as string, {
        status: "active",
      });
      break;
    }

    case "invoice.payment_failed": {
      const invoice = obj;
      if (!invoice.subscription) break;

      await updateSubscriptionInDB(invoice.subscription as string, {
        status: "past_due",
      });
      break;
    }

    case "customer.subscription.updated": {
      const stripeSub = obj as unknown as Stripe.Subscription;
      const userId = stripeSub.metadata?.userId;
      const sub = stripeSub as unknown as Record<string, unknown>;

      if (!userId) {
        const [existingSub] = await db
          .select()
          .from(subscriptions)
          .where(eq(subscriptions.stripeId, stripeSub.id))
          .limit(1);

        if (!existingSub) break;
      }

      const tier = stripeSub.items.data[0]?.price?.id
        ? mapStripeProductToTier(stripeSub.items.data[0].price.id)
        : undefined;

      await updateSubscriptionInDB(stripeSub.id, {
        tier,
        status: sub.status as string,
        currentPeriodStart: new Date((sub.current_period_start as number) * 1000),
        currentPeriodEnd: new Date((sub.current_period_end as number) * 1000),
        cancelAtPeriodEnd: sub.cancel_at_period_end as boolean,
      });
      break;
    }

    case "customer.subscription.deleted": {
      const stripeSub = obj as unknown as Stripe.Subscription;
      await updateSubscriptionInDB(stripeSub.id, {
        status: "canceled",
      });
      break;
    }
  }
}

function mapStripeProductToTier(priceId: string): Tier {
  if (priceId === process.env.STRIPE_PRICE_BASIC) return "basic";
  if (priceId === process.env.STRIPE_PRICE_PLUS) return "plus";
  if (priceId === process.env.STRIPE_PRICE_PREMIUM) return "premium";
  return "basic";
}
