/**
 * Session gate for the Smart Gallery AI routes.
 *
 * ---------------------------------------------------------------------------
 * TRUST MODEL
 * ---------------------------------------------------------------------------
 * These routes proxy a paid, rate-limited GPU backend (RunPod) using a secret
 * held only on this server. An unauthenticated route is therefore not merely an
 * information-disclosure problem — it is a billable-resource problem: anyone who
 * can reach the URL can spend the operator's GPU budget, and can use the CRM as
 * an open relay for arbitrary image/audio processing.
 *
 * The upstream SDK demo's routes (apps/web/src/app/api/ai/*) are COMPLETELY
 * unauthenticated. This module is the fix; every ported route must call it
 * before doing any work.
 *
 * WHO IS TRUSTED
 * We do not verify a JWT here and we do not hold any signing key. The single
 * source of truth for "is this caller signed in" is the Shell BFF
 * (`${BFF_ORIGIN}/api/session/context`), which owns the HttpOnly session cookie.
 * We forward the caller's raw `cookie` header to it and treat a 200 as proof of
 * a session. Consequences of that choice, stated explicitly:
 *
 *   - The BFF is trusted absolutely. If it is compromised or misconfigured to
 *     answer 200 for anonymous callers, these routes are open. BFF_ORIGIN must
 *     therefore only ever point at an origin the operator controls.
 *   - We forward the cookie header verbatim and nothing else. No Authorization
 *     header, no bearer token, and never the RunPod key.
 *   - NOTHING IS CACHED. A cached "yes" would keep a revoked/expired session
 *     alive for the cache lifetime, so every AI request costs one BFF round
 *     trip. That is deliberate: correctness over latency for a spend gate.
 *   - A network failure reaching the BFF returns 503 (fail CLOSED), never 200.
 *     If we cannot prove a session, we do not spend GPU budget.
 *
 * DEMO MODE
 * When the Shell is not configured (`NEXT_PUBLIC_SUPABASE_URL` unset) the CRM
 * runs on its mock portal and there is no session to check, so we allow the
 * request and log ONCE at startup-of-first-use. This is the same gate
 * `isShellConfigured()` uses for mock-vs-real auth elsewhere in the app.
 * IMPORTANT: never deploy to a public origin with the Shell unconfigured AND a
 * real RUNPOD_API_KEY present — that combination is an open, billable endpoint.
 *
 * WHAT THIS IS NOT
 * This is authentication only, not authorization. It answers "is there a valid
 * session", not "may this principal use the gallery". Per-resource policy for
 * gallery data lives in be-crm behind the `crm.gallery` resource; if these AI
 * routes ever need the same, check it there rather than re-deriving it here.
 */

export type GallerySession =
  | { ok: true; principalId?: string }
  | { ok: false; status: number; error: string };

/** Mirrors src/lib/appshell.ts — kept local so this stays server-only. */
function isShellConfigured(): boolean {
  return Boolean(process.env.NEXT_PUBLIC_SUPABASE_URL);
}

let demoModeWarned = false;

/**
 * Resolve the caller's session. Returns `{ ok: true }` (optionally with the
 * principal id, used to key rate limits) or a ready-to-return failure with the
 * status and message the route should emit.
 */
export async function requireGallerySession(req: Request): Promise<GallerySession> {
  if (!isShellConfigured()) {
    if (!demoModeWarned) {
      demoModeWarned = true;
      console.warn(
        "[gallery-ai] Shell is not configured (NEXT_PUBLIC_SUPABASE_URL unset) — " +
          "AI routes are UNAUTHENTICATED in demo mode. Do not expose this deployment publicly " +
          "while RUNPOD_API_KEY is set.",
      );
    }
    return { ok: true };
  }

  const cookie = req.headers.get("cookie");
  if (!cookie) return { ok: false, status: 401, error: "Not signed in" };

  const origin = (process.env.BFF_ORIGIN ?? "http://localhost:4000").replace(/\/$/, "");

  let res: Response;
  try {
    res = await fetch(`${origin}/api/session/context`, {
      headers: { cookie, accept: "application/json" },
      // Never cache an auth decision — see the trust-model note above.
      cache: "no-store",
      signal: AbortSignal.timeout(5000),
    });
  } catch {
    // Fail closed: we could not prove a session, so we do not spend GPU budget.
    return { ok: false, status: 503, error: "Session service unavailable" };
  }

  if (res.status === 401) return { ok: false, status: 401, error: "Not signed in" };
  if (!res.ok) return { ok: false, status: 503, error: "Session service unavailable" };

  // The principal id is best-effort: it only sharpens the rate-limit key, so a
  // shape we don't recognize degrades to IP-keyed limiting rather than failing.
  let principalId: string | undefined;
  try {
    const data = (await res.json()) as Record<string, unknown> | null;
    principalId = pickPrincipalId(data);
  } catch {
    /* ignore — see above */
  }

  return principalId ? { ok: true, principalId } : { ok: true };
}

function pickPrincipalId(data: Record<string, unknown> | null): string | undefined {
  if (!data) return undefined;
  const direct = data.userId ?? data.principalId ?? data.sub ?? data.id;
  if (typeof direct === "string" && direct) return direct;
  const user = data.user;
  if (user && typeof user === "object") {
    const u = user as Record<string, unknown>;
    const nested = u.id ?? u.userId ?? u.sub;
    if (typeof nested === "string" && nested) return nested;
  }
  return undefined;
}

/**
 * Rate-limit key for a request: the authenticated principal when known,
 * otherwise the first hop of `x-forwarded-for`.
 *
 * NOTE the first hop is client-controlled unless a trusted proxy overwrites the
 * header. It is good enough to throttle honest clients and casual abuse; it is
 * NOT a security boundary. The session gate above is the security boundary.
 */
export function rateLimitKey(req: Request, principalId?: string): string {
  if (principalId) return `u:${principalId}`;
  const xff = req.headers.get("x-forwarded-for") ?? "";
  const first = xff.split(",")[0]?.trim();
  return `ip:${first || "unknown"}`;
}
