// The Ampelos profile, as data.
//
// The companion to /embed/profile: charon can either frame the panel Ampelos
// renders, or take this and render the section itself in its own design system.
// Both come from getProfile(), so they cannot disagree about what somebody's
// Watch Now list contains.
//
// TWO WAYS TO AUTHENTICATE, and the difference matters:
//
//   1. The session cookie. accounts.sticknife.com is same-site with this host,
//      so a browser fetch with `credentials: "include"` carries the signed-in
//      user's session and this endpoint answers for THAT person only. Nothing
//      is trusted from the caller.
//
//   2. AMPELOS_PROFILE_API_TOKEN plus ?email= or ?userId=. For charon rendering
//      the panel server-side, where there is no browser to carry a cookie. The
//      caller names the user, so this is a full read of anybody's profile --
//      watch history included -- and it is exactly as sensitive as that sounds.
//
// Mode 2 has its OWN token rather than reusing AMPELOS_AGENT_TOKEN. That token
// is deployed to every satellite host, including ones nobody has physical
// control over; a herald script leaking it should not also hand over everyone's
// viewing history. Mode 2 stays off entirely until the variable is set.

import { timingSafeEqual } from "node:crypto";

import { eq } from "drizzle-orm";

import { auth } from "@/auth";
import { db } from "@/db/client";
import { users } from "@/db/schema";
import { APP_URL } from "@/lib/app-url";
import { isAllowedOrigin } from "@/lib/embed-origins";
import { getProfile } from "@/lib/profile";

export const dynamic = "force-dynamic";

function corsHeaders(request: Request) {
  const origin = request.headers.get("origin");
  const headers = new Headers();

  // Vary regardless of the outcome: the response differs by Origin, and a cache
  // that missed that would hand one site's allowance to another.
  headers.set("Vary", "Origin");

  if (isAllowedOrigin(origin)) {
    headers.set("Access-Control-Allow-Origin", origin);
    // Required for the cookie to be sent at all, and the reason the allowed
    // origin is an explicit list rather than "*" -- the two cannot be combined.
    headers.set("Access-Control-Allow-Credentials", "true");
  }

  return headers;
}

function tokenMatches(request: Request, expected: string) {
  const header = request.headers.get("authorization") ?? "";
  const provided = header.startsWith("Bearer ") ? header.slice(7) : "";
  const a = Buffer.from(provided);
  const b = Buffer.from(expected);
  return Boolean(provided) && a.length === b.length && timingSafeEqual(a, b);
}

/**
 * Whose profile is being asked for, and may the caller have it?
 *
 * Returns a user id or a refusal. The session path never reads the query
 * string, so a signed-in user cannot widen their own request by adding
 * ?email= to it.
 */
async function resolveUserId(request: Request): Promise<
  { ok: true; userId: string } | { ok: false; status: number; error: string }
> {
  const session = await auth();
  if (session?.user?.id) return { ok: true, userId: session.user.id };

  const expected = process.env.AMPELOS_PROFILE_API_TOKEN;
  if (!expected) return { ok: false, status: 401, error: "not signed in" };
  if (!tokenMatches(request, expected)) return { ok: false, status: 401, error: "not signed in" };

  const params = new URL(request.url).searchParams;
  const userId = params.get("userId");
  const email = params.get("email");

  if (userId) return { ok: true, userId };

  if (email) {
    const [row] = await db
      .select({ id: users.id })
      .from(users)
      // users.email is unique and is the claim Authentik issues to every
      // sticknife service, which makes it the one identifier charon already
      // holds without Ampelos telling it anything.
      .where(eq(users.email, email))
      .limit(1);
    if (!row) return { ok: false, status: 404, error: "no such user" };
    return { ok: true, userId: row.id };
  }

  return { ok: false, status: 400, error: "name a user with ?userId= or ?email=" };
}

export async function GET(request: Request) {
  const headers = corsHeaders(request);
  const resolved = await resolveUserId(request);

  if (!resolved.ok) {
    return Response.json({ error: resolved.error }, { status: resolved.status, headers });
  }

  const profile = await getProfile(resolved.userId, APP_URL);
  if (!profile) {
    return Response.json({ error: "no such user" }, { status: 404, headers });
  }

  // Somebody's viewing history is not something to leave in a shared cache.
  headers.set("Cache-Control", "private, no-store");
  return Response.json(profile, { headers });
}

export async function OPTIONS(request: Request) {
  const headers = corsHeaders(request);
  headers.set("Access-Control-Allow-Methods", "GET, OPTIONS");
  headers.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
  headers.set("Access-Control-Max-Age", "86400");
  return new Response(null, { status: 204, headers });
}
