"use server";

// Ask for a better copy of one episode.
//
// Three things happen, and they are deliberately three rather than one big
// one: the file is flagged, the episode's search cooldown is cleared, and the
// page is revalidated. What does NOT happen here is the download. The fetch
// loop owns searching and grabbing, it applies the identity gate and the size
// rules, and reproducing any of that in a server action would mean two places
// deciding what is worth grabbing.
//
// So the button's promise is: this copy is marked, the episode is at the front
// of the queue, and the next fetch cycle will go looking. Not "it is
// downloading now".

import { auth } from "@/auth";
import { db } from "@/db/client";
import { searchRuns, storageFiles, storageTiers } from "@/db/schema";
import { and, eq, isNull, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

async function requireAdmin() {
  const session = await auth();
  if (!session) redirect("/login");
  if (!session.user.isAdmin) redirect("/");
  return session;
}

function optionalString(value: FormDataEntryValue | null) {
  return typeof value === "string" && value.trim() ? value.trim() : null;
}

export async function requestReplacement(formData: FormData) {
  const session = await requireAdmin();
  const fileId = optionalString(formData.get("fileId"));
  const seriesId = optionalString(formData.get("seriesId"));
  if (!fileId) return;

  // Live only. A request is about the copy being served; flagging a backup or
  // archive file would mark something the reaper will never touch and quietly
  // do nothing at all.
  const [flagged] = await db
    .update(storageFiles)
    .set({ replaceRequestedAt: new Date(), replaceRequestedBy: session.user.id })
    .where(and(
      eq(storageFiles.id, fileId),
      isNull(storageFiles.missingAt),
      sql`${storageFiles.tierId} in (select id from ${storageTiers} where tier = 'live')`,
    ))
    .returning({ episodeId: storageFiles.episodeId, mediaItemId: storageFiles.mediaItemId });

  // Clear the search cooldown so the next cycle actually looks. Without this a
  // title searched in the last six hours would be skipped, and the button
  // would appear to do nothing for most of a working day.
  if (flagged?.mediaItemId) {
    await db.delete(searchRuns).where(eq(searchRuns.mediaItemId, flagged.mediaItemId));
  }

  if (seriesId) revalidatePath(`/admin/inventory/series/${seriesId}`);
  revalidatePath("/admin/inventory");
}

/** Change your mind. Clears the flag, and with it the queue entry and the reaper's licence. */
export async function cancelReplacement(formData: FormData) {
  await requireAdmin();
  const fileId = optionalString(formData.get("fileId"));
  const seriesId = optionalString(formData.get("seriesId"));
  if (!fileId) return;

  await db
    .update(storageFiles)
    .set({ replaceRequestedAt: null, replaceRequestedBy: null })
    .where(eq(storageFiles.id, fileId));

  if (seriesId) revalidatePath(`/admin/inventory/series/${seriesId}`);
  revalidatePath("/admin/inventory");
}
