// src/lib/auth/seat-guard.ts — accept-time seat guard (ELEVA-112 S2).
// An invitation sent while a seat was free can be accepted later when the org
// is full (seat taken, downgrade, trial expired). The auth hook re-checks
// seats+billing here and, when blocked, leaves the invitation pending, throws
// SEAT_BLOCKED to the client, and nudges the org's owner/admins by email
// (deduped via invitation.seatBlockNotifiedAt).
import "server-only";
import { and, eq, inArray, isNull } from "drizzle-orm";
import { assertWithinQuota, QuotaError } from "@/lib/billing/quotas";
import { getOrgBillingInfo } from "@/lib/billing/status";
import { invitation, member, organization, user } from "@/lib/db/auth-schema";
import { db } from "@/lib/db/client";
import { sendSeatBlockedEmail } from "@/lib/email";

export type SeatBlockCheck =
  | { blocked: false }
  | {
      blocked: true;
      invitationId: string;
      orgId: string;
      orgName: string;
      /** Plan of the org's relevant subscription — null on trial/expired. */
      plan: string | null;
      /** Address of the person trying to join (the invitation's email). */
      attemptEmail: string;
      /** The admin nudge was already sent for this invitation. */
      alreadyNotified: boolean;
    };

/**
 * Would accepting this pending invitation exceed the org's seats, or is the
 * org expired? Unknown / non-pending invitations are NOT blocked — Better
 * Auth's own endpoint validation answers those.
 */
export async function checkInvitationSeatBlock(
  invitationId: string,
): Promise<SeatBlockCheck> {
  const [inv] = await db
    .select({
      id: invitation.id,
      orgId: invitation.organizationId,
      email: invitation.email,
      seatBlockNotifiedAt: invitation.seatBlockNotifiedAt,
      orgName: organization.name,
    })
    .from(invitation)
    .innerJoin(organization, eq(organization.id, invitation.organizationId))
    .where(
      and(eq(invitation.id, invitationId), eq(invitation.status, "pending")),
    )
    .limit(1);
  if (!inv) return { blocked: false };

  const billing = await getOrgBillingInfo(inv.orgId);
  let blocked = billing.status === "expired";
  if (!blocked) {
    try {
      // Increment 0 on purpose: countOrgSeats already counts THIS pending
      // invitation as a reserved seat, and accepting only converts the
      // reservation into a membership (net seat change = 0). A +1 here would
      // falsely block the invite that legitimately took the last seat.
      await assertWithinQuota(inv.orgId, "seats", 0);
    } catch (e) {
      if (!(e instanceof QuotaError)) throw e;
      blocked = true;
    }
  }
  if (!blocked) return { blocked: false };
  return {
    blocked: true,
    invitationId: inv.id,
    orgId: inv.orgId,
    orgName: inv.orgName,
    plan: billing.plan,
    attemptEmail: inv.email,
    alreadyNotified: inv.seatBlockNotifiedAt !== null,
  };
}

/**
 * Send the "no seat left — upgrade to welcome them" nudge to the org's
 * owner + admins, exactly once per blocked invitation: the dedup stamp
 * (`seatBlockNotifiedAt`) is written first with an IS NULL guard, so a
 * concurrent double-accept only sends one email. Returns whether this call
 * won the stamp (and therefore sent).
 *
 * `send` is injectable for tests (no real Resend call).
 */
export async function notifySeatBlock(
  block: Extract<SeatBlockCheck, { blocked: true }>,
  send: typeof sendSeatBlockedEmail = sendSeatBlockedEmail,
): Promise<boolean> {
  const won = await db
    .update(invitation)
    .set({ seatBlockNotifiedAt: new Date() })
    .where(
      and(
        eq(invitation.id, block.invitationId),
        isNull(invitation.seatBlockNotifiedAt),
      ),
    )
    .returning({ id: invitation.id });
  if (won.length === 0) return false;

  const recipients = await db
    .select({ email: user.email })
    .from(member)
    .innerJoin(user, eq(user.id, member.userId))
    .where(
      and(
        eq(member.organizationId, block.orgId),
        inArray(member.role, ["owner", "admin"]),
      ),
    );
  for (const r of recipients) {
    await send({
      to: r.email,
      orgName: block.orgName,
      plan: block.plan,
      attemptEmail: block.attemptEmail,
    });
  }
  return true;
}
