import type Stripe from "stripe";

import { eq } from "drizzle-orm";

import { isSupportedCurrency, type Currency } from "@/lib/billing/currency";
import { BILLABLE_PLANS, SEAT_ADDON } from "@/lib/billing/plans";
import { db } from "@/lib/db";
import { orgSubscription, type BillingInterval, type Plan, type SubStatus } from "@/lib/db/schema";

/* Reverse lookup: Stripe price ID → our (plan, interval). Built once at
   module load from BILLABLE_PLANS.stripePriceIds. */
const PRICE_ID_TO_PLAN: Map<string, { plan: Plan; interval: BillingInterval }> = (() => {
  const m = new Map<string, { plan: Plan; interval: BillingInterval }>();
  for (const tier of Object.keys(BILLABLE_PLANS) as Array<keyof typeof BILLABLE_PLANS>) {
    const ids = BILLABLE_PLANS[tier].stripePriceIds;
    if (!ids) continue;
    if (ids.month) m.set(ids.month, { plan: tier, interval: "month" });
    if (ids.year) m.set(ids.year, { plan: tier, interval: "year" });
  }
  return m;
})();

/* Reverse lookup: Stripe price ID → "seat_addon". Used as a fallback when
   a subscription item lacks `price.metadata.kind` (e.g. legacy / dashboard-
   created Prices) so old single-item subs keep working. */
const SEAT_ADDON_PRICE_IDS: ReadonlySet<string> = (() => {
  const s = new Set<string>();
  if (SEAT_ADDON.stripePriceIds.month) s.add(SEAT_ADDON.stripePriceIds.month);
  if (SEAT_ADDON.stripePriceIds.year) s.add(SEAT_ADDON.stripePriceIds.year);
  return s;
})();

type ItemKind = "tier" | "seat_addon";

/* Classify a subscription item as a tier item or a seat-addon item.
   Prefers explicit `price.metadata.kind`; falls back to matching the price
   id against the known PRICE_ID_TO_PLAN / SEAT_ADDON_PRICE_IDS maps so that
   pre-multi-currency subscriptions (which lack the kind metadata) keep
   working. Returns null if the item can't be classified. */
function classifyItem(item: Stripe.SubscriptionItem): ItemKind | null {
  const kindMeta = item.price?.metadata?.kind;
  if (kindMeta === "tier" || kindMeta === "seat_addon") return kindMeta;

  const priceId = item.price?.id;
  if (!priceId) return null;
  if (SEAT_ADDON_PRICE_IDS.has(priceId)) return "seat_addon";
  if (PRICE_ID_TO_PLAN.has(priceId)) return "tier";
  return null;
}

export async function handleStripeEvent(event: Stripe.Event): Promise<void> {
  switch (event.type) {
    case "checkout.session.completed":
      await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session);
      break;
    case "customer.subscription.updated":
    case "customer.subscription.deleted":
      await handleSubChange(event.data.object as Stripe.Subscription);
      break;
    case "invoice.payment_failed":
    case "invoice.paid":
      await handleInvoice(event.data.object as Stripe.Invoice);
      break;
    case "customer.updated":
      // No-op v1 (email/name sync not critical).
      break;
    default:
      break;
  }
}

async function handleCheckoutCompleted(session: Stripe.Checkout.Session): Promise<void> {
  const orgId = (session.client_reference_id || (session.metadata?.orgId as string | undefined)) as
    | string
    | undefined;
  if (!orgId) throw new Error("checkout.session.completed missing orgId");
  if (!session.subscription) throw new Error("checkout.session.completed missing subscription id");

  const subscriptionId =
    typeof session.subscription === "string" ? session.subscription : session.subscription.id;

  const { getStripe } = await import("@/lib/billing/stripe");
  const sub = await getStripe().subscriptions.retrieve(subscriptionId, {
    expand: ["items.data.price"],
  });

  await applySubscriptionToOrg(orgId, sub);
}

async function handleSubChange(sub: Stripe.Subscription): Promise<void> {
  const customerId = typeof sub.customer === "string" ? sub.customer : sub.customer.id;
  const orgId = await orgIdFromCustomerId(customerId);
  if (!orgId) return;
  await applySubscriptionToOrg(orgId, sub);
}

async function handleInvoice(invoice: Stripe.Invoice): Promise<void> {
  if (!invoice.customer) return;
  const customerId = typeof invoice.customer === "string" ? invoice.customer : invoice.customer.id;
  const orgId = await orgIdFromCustomerId(customerId);
  if (!orgId) return;

  if (invoice.status === "paid") {
    await db
      .update(orgSubscription)
      .set({ status: "active", updatedAt: new Date() })
      .where(eq(orgSubscription.organizationId, orgId));
  } else if (invoice.status === "open" || (invoice.attempt_count ?? 0) > 0) {
    await db
      .update(orgSubscription)
      .set({ status: "past_due", updatedAt: new Date() })
      .where(eq(orgSubscription.organizationId, orgId));
  }
}

export async function applySubscriptionToOrg(
  orgId: string,
  sub: Stripe.Subscription,
): Promise<void> {
  /* Walk every subscription item and split them into tier vs seat addon.
     The plan calls for exactly ONE tier item (a malformed subscription —
     zero or multiple tier items — must throw so the Stripe webhook retries
     it; we never want to silently downgrade an org). The seat addon item
     is optional; absent ⇒ extraSeats = 0. */
  const tierItems: Stripe.SubscriptionItem[] = [];
  let seatItem: Stripe.SubscriptionItem | null = null;
  for (const item of sub.items.data) {
    const kind = classifyItem(item);
    if (kind === "tier") {
      tierItems.push(item);
    } else if (kind === "seat_addon") {
      seatItem = item;
    }
  }

  if (tierItems.length !== 1) {
    const summary = sub.items.data.map((i) => ({
      id: i.id,
      priceId: i.price?.id,
      kind: i.price?.metadata?.kind ?? null,
    }));
    const reason = tierItems.length === 0 ? "no tier item" : "multiple tier items";
    console.error("[stripe-webhook] malformed subscription", {
      subscriptionId: sub.id,
      orgId,
      reason,
      items: summary,
    });
    throw new Error(
      `Malformed Stripe subscription ${sub.id}: ${reason} (expected exactly 1 item with price.metadata.kind="tier")`,
    );
  }

  const tierItem = tierItems[0];
  const priceId = tierItem.price?.id;
  const mapped = priceId ? PRICE_ID_TO_PLAN.get(priceId) : undefined;
  if (!priceId || !mapped) {
    console.error("[stripe-webhook] unknown tier price id", {
      subscriptionId: sub.id,
      orgId,
      priceId,
    });
    throw new Error(
      `Stripe subscription ${sub.id} references unknown tier price id ${priceId ?? "<missing>"}`,
    );
  }
  const plan: Plan = mapped.plan;
  const interval: BillingInterval = mapped.interval;

  /* Currency lives on the subscription itself (Stripe stores the active
     billing currency there as the lowercased ISO 4217 code). Validate
     against the app's supported set; on mismatch, fall back to "eur"
     defensively rather than throwing — the webhook should never crash
     because of an unexpected currency code. */
  const rawCurrency: unknown = sub.currency;
  const currency: Currency = isSupportedCurrency(rawCurrency) ? rawCurrency : "eur";
  if (!isSupportedCurrency(rawCurrency)) {
    console.warn("[stripe-webhook] unsupported subscription currency, falling back to eur", {
      subscriptionId: sub.id,
      orgId,
      rawCurrency,
    });
  }

  const extraSeats = seatItem?.quantity ?? 0;

  const isCanceled = sub.status === "canceled";
  const status: SubStatus = (sub.status as SubStatus) ?? "active";

  // Stripe 2026-04-22.dahlia API moves period dates to the item level for new
  // subscriptions. Fall back to the deprecated top-level fields if items
  // don't expose them on the bundled types.
  const itemAny = tierItem as unknown as {
    current_period_start?: number;
    current_period_end?: number;
  };
  const subAny = sub as unknown as {
    current_period_start?: number;
    current_period_end?: number;
  };
  const startTs = itemAny.current_period_start ?? subAny.current_period_start ?? null;
  const endTs = itemAny.current_period_end ?? subAny.current_period_end ?? null;

  const periodStart = startTs ? new Date(startTs * 1000) : null;
  const periodEnd = endTs ? new Date(endTs * 1000) : null;
  const canceledAt = sub.canceled_at ? new Date(sub.canceled_at * 1000) : null;

  await db
    .update(orgSubscription)
    .set({
      plan: isCanceled ? "free" : plan,
      interval: isCanceled ? null : interval,
      currency,
      extraSeats: isCanceled ? 0 : extraSeats,
      stripeSubscriptionId: sub.id,
      stripePriceId: priceId,
      status,
      currentPeriodStart: periodStart,
      currentPeriodEnd: periodEnd,
      cancelAtPeriodEnd: sub.cancel_at_period_end,
      canceledAt,
      updatedAt: new Date(),
    })
    .where(eq(orgSubscription.organizationId, orgId));
}

async function orgIdFromCustomerId(customerId: string): Promise<string | null> {
  const rows = await db
    .select({ orgId: orgSubscription.organizationId })
    .from(orgSubscription)
    .where(eq(orgSubscription.stripeCustomerId, customerId))
    .limit(1);
  return rows[0]?.orgId ?? null;
}
