import "server-only";
import { randomBytes } from "node:crypto";
import { z } from "zod";
import type { ApplicationProviders } from "./application-providers";
import { ApplicationError } from "./application-record";
export const STAFF_SESSION_SECONDS = 8 * 3600;
const claimsSchema = z.object({
  uid: z.string().min(1).max(128),
  email: z.email(),
  email_verified: z.literal(true),
  auth_time: z.number().int().nonnegative(),
});
const state = globalThis as typeof globalThis & {
  mockStaffSessions?: Map<
    string,
    { uid: string; email: string; expires: number }
  >;
};
const sessions = () => (state.mockStaffSessions ??= new Map());
export const staffCookieName = (mock: boolean) =>
  mock ? "maxcredit-staff" : "__Host-maxcredit-staff";
export function staffCookie(request: Request, mock: boolean) {
  return (
    request.headers
      .get("cookie")
      ?.split(";")
      .map((value) => value.trim())
      .find((value) => value.startsWith(`${staffCookieName(mock)}=`))
      ?.slice(staffCookieName(mock).length + 1) ?? ""
  );
}
function allowed(email: string, providers: ApplicationProviders) {
  if (!providers.config.STAFF_EMAIL_ALLOWLIST.includes(email.toLowerCase()))
    throw new ApplicationError(403, "This staff account is not authorized.");
}
export async function createStaffSession(
  token: string,
  providers: ApplicationProviders,
) {
  if (providers.mock) {
    if (token !== "mock-staff-reviewed-identity")
      throw new ApplicationError(401, "Staff sign-in failed.");
    const email = "reviewer@example.invalid";
    allowed(email, providers);
    for (const [key, value] of sessions())
      if (value.expires <= Date.now()) sessions().delete(key);
    if (sessions().size >= 1000)
      throw new ApplicationError(429, "Too many local sessions.");
    const cookie = randomBytes(32).toString("hex");
    sessions().set(cookie, {
      uid: "mock-reviewer",
      email,
      expires: Date.now() + STAFF_SESSION_SECONDS * 1000,
    });
    return cookie;
  }
  if (!providers.auth)
    throw new ApplicationError(503, "Staff sign-in is unavailable.");
  let claims;
  try {
    claims = claimsSchema.parse(
      await providers.auth.verifyIdToken(token, true),
    );
  } catch {
    throw new ApplicationError(401, "Staff sign-in failed. Sign in again.");
  }
  allowed(claims.email, providers);
  const age = Date.now() / 1000 - claims.auth_time;
  if (age < 0 || age > 300)
    throw new ApplicationError(
      401,
      "Please sign in again to start a fresh session.",
    );
  return providers.auth.createSessionCookie(token, {
    expiresIn: STAFF_SESSION_SECONDS * 1000,
  });
}
export async function requireStaff(
  cookie: string,
  providers: ApplicationProviders,
): Promise<{ uid: string; email: string }> {
  if (!cookie || cookie.length > 8192)
    throw new ApplicationError(401, "Staff sign-in is required.");
  if (providers.mock) {
    const session = sessions().get(cookie);
    if (!session || session.expires <= Date.now()) {
      sessions().delete(cookie);
      throw new ApplicationError(401, "Your session expired. Sign in again.");
    }
    allowed(session.email, providers);
    return { uid: session.uid, email: session.email };
  }
  let claims;
  try {
    claims = claimsSchema.parse(
      await providers.auth!.verifySessionCookie(cookie, true),
    );
  } catch {
    throw new ApplicationError(
      401,
      "Your session expired or was revoked. Sign in again.",
    );
  }
  allowed(claims.email, providers);
  return { uid: claims.uid, email: claims.email };
}
export async function endStaffSession(
  cookie: string,
  providers: ApplicationProviders,
) {
  if (providers.mock) {
    sessions().delete(cookie);
    return;
  }
  const staff = await requireStaff(cookie, providers);
  await providers.auth!.revokeRefreshTokens(staff.uid);
}
