// src/lib/billing/seat-unblock.ts — seat flow S3: close the loop on upgrade.
// An invitee blocked at acceptance (seat-guard, S2) left a pending invitation
// stamped with `seatBlockNotifiedAt`. When the org's Stripe subscription is
// created/updated (plan upgrade), free seats are recomputed and the oldest
// blocked invitees get a "a seat is now free" email + their stamp reset to
// null — so a later re-block can re-notify the admin (S2 dedup restarts).
// Spec: docs/superpowers/specs/2026-06-12-seat-flow-design.md (S3).
import "server-only";
import { and, eq, isNotNull } from "drizzle-orm";
import type Stripe from "stripe";
import { invitation, organization, subscription } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { sendSeatFreedEmail } from "@/lib/email";
import { getOrgQuotaLimits } from "./quotas";
import { countOrgSeats } from "./usage";

export interface BlockedInvitee {
  id: string;
  email: string;
  /** When the S2 admin "no seat left" email was sent for this invitation. */
  seatBlockNotifiedAt: Date;
}

/**
 * Pure selection (unit-testable): who gets the "seat freed" email?
 * - `freeSeats` of -1 or Infinity (unlimited plan) → everyone;
 * - `freeSeats` <= 0 → nobody;
 * - otherwise the longest-waiting first (oldest `seatBlockNotifiedAt`),
 *   capped at `freeSeats`.
 */
export function selectInviteesToUnblock(
  freeSeats: number,
  blocked: BlockedInvitee[],
): BlockedInvitee[] {
  const oldestFirst = [...blocked].sort(
    (a, b) => a.seatBlockNotifiedAt.getTime() - b.seatBlockNotifiedAt.getTime(),
  );
  if (freeSeats === -1 || freeSeats === Number.POSITIVE_INFINITY) {
    return oldestFirst;
  }
  if (freeSeats <= 0) return [];
  return oldestFirst.slice(0, freeSeats);
}

/**
 * The event's subscription → our `subscription` row (written by the Stripe
 * plugin's own handler, which runs BEFORE `onEvent`) → `referenceId` = org id.
 * Falls back to the subscription's `metadata.referenceId` (set at checkout)
 * for events arriving before the row exists.
 */
async function resolveOrgId(sub: Stripe.Subscription): Promise<string | null> {
  const [row] = await db
    .select({ referenceId: subscription.referenceId })
    .from(subscription)
    .where(eq(subscription.stripeSubscriptionId, sub.id))
    .limit(1);
  if (row) return row.referenceId;
  const ref = sub.metadata?.referenceId;
  return typeof ref === "string" && ref.length > 0 ? ref : null;
}

/**
 * Webhook-driven unblock: on `customer.subscription.created/updated`, email
 * the oldest seat-blocked invitees that a seat is now free and reset their
 * dedup stamp. Idempotent enough for webhook retries: the stamp is cleared
 * with an IS NOT NULL guard BEFORE sending (same pattern as notifySeatBlock),
 * so a concurrent retry can't double-email; a later re-block re-stamps.
 *
 * `send` is injectable for tests (no real Resend call).
 */
export async function unblockSeatInvitations(
  event: Stripe.Event,
  send: typeof sendSeatFreedEmail = sendSeatFreedEmail,
): Promise<void> {
  if (
    event.type !== "customer.subscription.updated" &&
    event.type !== "customer.subscription.created"
  ) {
    return;
  }
  const orgId = await resolveOrgId(event.data.object);
  if (!orgId) return;

  const limits = await getOrgQuotaLimits(orgId);
  const free =
    limits.seats === -1
      ? Number.POSITIVE_INFINITY
      : limits.seats - (await countOrgSeats(orgId));
  if (free <= 0) return;

  const [org] = await db
    .select({ name: organization.name })
    .from(organization)
    .where(eq(organization.id, orgId))
    .limit(1);
  if (!org) return;

  const rows = await db
    .select({
      id: invitation.id,
      email: invitation.email,
      seatBlockNotifiedAt: invitation.seatBlockNotifiedAt,
    })
    .from(invitation)
    .where(
      and(
        eq(invitation.organizationId, orgId),
        eq(invitation.status, "pending"),
        isNotNull(invitation.seatBlockNotifiedAt),
      ),
    );
  // The IS NOT NULL predicate guarantees the stamp — narrow for the type.
  const blocked: BlockedInvitee[] = rows.flatMap((r) =>
    r.seatBlockNotifiedAt === null
      ? []
      : [
          {
            id: r.id,
            email: r.email,
            seatBlockNotifiedAt: r.seatBlockNotifiedAt,
          },
        ],
  );

  for (const inv of selectInviteesToUnblock(free, blocked)) {
    // Win the stamp first: only the call that actually cleared it sends.
    const won = await db
      .update(invitation)
      .set({ seatBlockNotifiedAt: null })
      .where(
        and(
          eq(invitation.id, inv.id),
          isNotNull(invitation.seatBlockNotifiedAt),
        ),
      )
      .returning({ id: invitation.id });
    if (won.length === 0) continue;
    await send({ to: inv.email, orgName: org.name, invitationId: inv.id });
  }
}
