"use server";

// Linking a Plex account.
//
// Two steps, because Plex's sign-in is a round trip through their site: we ask
// for a PIN, send the person to plex.tv to claim it, and pick the result up
// when they come back. The PIN id is parked in an httpOnly cookie for the
// duration -- it is a one-use handle that is worthless without the sign-in that
// claims it, and it means the flow survives the user taking a minute over it.
//
// Linking is what grants library access AND what makes a watchlist readable.
// Both follow from the same consent, which is why they happen together here
// rather than being two things an admin has to remember to do.

import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { eq } from "drizzle-orm";

import { auth } from "@/auth";
import { db } from "@/db/client";
import { plexAccounts } from "@/db/schema";
import { appUrl } from "@/lib/app-url";
import { createPin, authUrl, claimPin, shareLibraries } from "@/lib/plex";

const PIN_COOKIE = "ampelos_plex_pin";

async function requireUser() {
  const session = await auth();
  if (!session?.user?.id) throw new Error("Sign in first");
  return session.user;
}

export async function startPlexLinkAction() {
  await requireUser();

  const pin = await createPin();
  const jar = await cookies();
  jar.set(PIN_COOKIE, String(pin.id), {
    httpOnly: true,
    sameSite: "lax",
    secure: true,
    path: "/",
    maxAge: 15 * 60,
  });

  // redirect() throws, so it must be the last thing here.
  redirect(authUrl(pin, appUrl("/account?linking=1")));
}

/**
 * Finish the link, if the user has been to plex.tv and back.
 *
 * Returns a message rather than throwing on the ordinary failures -- an
 * abandoned sign-in and an expired PIN are both things a person does, not
 * errors.
 */
export async function completePlexLink(): Promise<string | null> {
  const user = await requireUser();
  const jar = await cookies();
  const pinId = jar.get(PIN_COOKIE)?.value;
  if (!pinId) return null;

  let identity;
  try {
    identity = await claimPin(Number(pinId));
  } catch (error) {
    jar.delete(PIN_COOKIE);
    return `Plex could not confirm the sign-in: ${(error as Error).message}`;
  }
  if (!identity) return "That sign-in was not completed. Try linking again.";

  jar.delete(PIN_COOKIE);

  // One Plex account per person, in both directions. Without this a second
  // person could link an account already in use and inherit its watchlist.
  const existing = await db
    .select()
    .from(plexAccounts)
    .where(eq(plexAccounts.plexUserId, identity.plexUserId))
    .limit(1);
  if (existing.length && existing[0].userId !== user.id) {
    return "That Plex account is already linked to another Ampelos user.";
  }

  let shared: { sectionTitles: string[]; sectionIds: string[]; alreadyShared: boolean } | null = null;
  let shareError: string | null = null;
  try {
    shared = await shareLibraries(identity);
  } catch (error) {
    // The link is still worth recording: it is what makes the watchlist
    // readable, and a share that failed can be retried without signing in
    // again.
    shareError = (error as Error).message;
  }

  await db
    .insert(plexAccounts)
    .values({
      userId: user.id,
      plexUserId: identity.plexUserId,
      plexUuid: identity.plexUuid,
      plexUsername: identity.username,
      plexEmail: identity.email,
      librariesSharedAt: shared ? new Date() : null,
      sharedSectionIds: shared?.sectionIds ?? null,
    })
    .onConflictDoUpdate({
      target: plexAccounts.userId,
      set: {
        plexUserId: identity.plexUserId,
        plexUuid: identity.plexUuid,
        plexUsername: identity.username,
        plexEmail: identity.email,
        librariesSharedAt: shared ? new Date() : null,
        sharedSectionIds: shared?.sectionIds ?? null,
        updatedAt: new Date(),
      },
    });

  revalidatePath("/account");

  if (shareError) {
    return `Linked as ${identity.username}, but the libraries could not be shared: ${shareError}`;
  }
  if (shared?.alreadyShared) {
    return `Linked as ${identity.username}. You already had access to the libraries.`;
  }
  return `Linked as ${identity.username}. Shared: ${shared?.sectionTitles.join(", ")}.`;
}

export async function unlinkPlexAction() {
  const user = await requireUser();
  // Only the link is removed. Library access is granted on Plex's side and is
  // not ours to quietly revoke from a button labelled "unlink"; whoever owns
  // the server can take it back there.
  await db.delete(plexAccounts).where(eq(plexAccounts.userId, user.id));
  revalidatePath("/account");
}
