// Finishing a Plex link: the part that touches the database.
//
// Deliberately knows nothing about cookies, redirects or revalidation. It used
// to run inside the render of the profile page, which is why linking never
// completed: a page render may not delete a cookie or call revalidatePath, so
// the moment Plex returned the user, the page threw. The caller is now a route
// handler, which is allowed to do both -- see src/app/api/plex/callback/route.ts.

import { eq } from "drizzle-orm";

import { db } from "@/db/client";
import { plexAccounts } from "@/db/schema";
import { claimPin, ownerAccountId, shareLibraries } from "@/lib/plex";

/**
 * Where the in-flight PIN id is parked between starting a link and finishing
 * it. Lives here rather than in the route handler because a route.ts may only
 * export handlers and route config -- exporting a constant from one trips
 * Next's type check on the module.
 */
export const PIN_COOKIE = "ampelos_plex_pin";

/**
 * Outcome codes. These travel back to the profile page in the query string, so
 * they are short, stable, and carry no detail that would be wrong to put in a
 * URL somebody might paste.
 */
export type PlexLinkOutcome =
  | "linked"
  | "owner"
  | "already-shared"
  | "share-failed"
  | "not-completed"
  | "taken"
  | "error";

export type PlexLinkResult = {
  outcome: PlexLinkOutcome;
  username?: string;
  /** Only for the failure cases, and only ever a short reason. */
  reason?: string;
};

export async function completePlexLink(userId: string, pinId: number): Promise<PlexLinkResult> {
  let identity;
  try {
    identity = await claimPin(pinId);
  } catch (error) {
    return { outcome: "error", reason: (error as Error).message };
  }

  // An abandoned sign-in and an expired PIN are both things a person does, not
  // errors: the PIN is simply still unclaimed.
  if (!identity) return { outcome: "not-completed" };

  // 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 !== userId) {
    return { outcome: "taken", username: identity.username };
  }

  // THE OWNER IS NOT INVITED TO THEIR OWN SERVER. Plex answers a share request
  // naming them with "You cannot send an invitation to yourself", and reporting
  // that as a failed share would tell the one person who definitely has access
  // that they have none, on every visit, forever.
  //
  // Checked before attempting the share rather than by matching that sentence
  // afterwards: the wording belongs to Plex and may change, and a share request
  // that was never going to succeed is not worth making.
  let isOwner = false;
  try {
    const owner = await ownerAccountId();
    isOwner = owner !== null && owner === identity.plexUserId;
  } catch {
    // Unreachable plex.tv means we cannot tell. Falling through to the share
    // attempt is the safe direction: a non-owner still gets their invitation,
    // and an owner gets the old confusing message rather than a wrong claim of
    // access.
  }

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

  const values = {
    plexUserId: identity.plexUserId,
    plexUuid: identity.plexUuid,
    plexUsername: identity.username,
    plexEmail: identity.email,
    isServerOwner: isOwner,
    // Both stay null for the owner: nothing was shared with them because
    // nothing needed to be.
    librariesSharedAt: shared ? new Date() : null,
    sharedSectionIds: shared?.sectionIds ?? null,
  };

  await db
    .insert(plexAccounts)
    .values({ userId, ...values })
    .onConflictDoUpdate({
      target: plexAccounts.userId,
      set: { ...values, updatedAt: new Date() },
    });

  if (isOwner) return { outcome: "owner", username: identity.username };
  if (shareError) return { outcome: "share-failed", username: identity.username, reason: shareError };
  if (shared?.alreadyShared) return { outcome: "already-shared", username: identity.username };
  return { outcome: "linked", username: identity.username };
}
