"use server";

import { auth } from "@/auth";
import { db } from "@/db/client";
import { classics, externalIds, mediaItems, users, watchingNowItems } from "@/db/schema";
import { ensureMediaItem, findMediaItemByTmdbId } from "@/lib/media-item";
import { and, eq, isNull, ne } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

type MediaType = "tv_series" | "movie";

type WatchKind = "television" | "movies";

type WatchNowResult = { ok: true } | { ok: false; message: string };

function validDate(value: FormDataEntryValue | null) {
  if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
    return null;
  }

  return value;
}

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

function mediaTypeFromKind(kind: string | null): MediaType | null {
  if (kind === "movies") {
    return "movie";
  }

  if (kind === "television") {
    return "tv_series";
  }

  return null;
}

function kindFromMediaType(mediaType: MediaType): WatchKind {
  return mediaType === "movie" ? "movies" : "television";
}

function parseSlot(value: FormDataEntryValue | null) {
  const slot = typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
  return Number.isInteger(slot) && slot > 0 ? slot : null;
}

async function matchingClassic(mediaType: MediaType, tmdbId: string, title: string, year: number | null) {
  const rows = await db
    .select({
      mediaItemId: mediaItems.id,
      title: mediaItems.title,
      year: mediaItems.year,
      externalId: externalIds.externalId,
    })
    .from(classics)
    .innerJoin(mediaItems, eq(mediaItems.id, classics.mediaItemId))
    .leftJoin(externalIds, and(eq(externalIds.mediaItemId, mediaItems.id), eq(externalIds.source, "tmdb")))
    .where(eq(mediaItems.mediaType, mediaType));

  const normalizedTitle = title.toLocaleLowerCase("en-US");
  return rows.find((row) =>
    row.externalId === tmdbId ||
    (row.title.toLocaleLowerCase("en-US") === normalizedTitle && (row.year ?? null) === year),
  );
}

async function mediaItemIsClassic(mediaItemId: string) {
  const [row] = await db
    .select({ id: classics.id })
    .from(classics)
    .where(eq(classics.mediaItemId, mediaItemId))
    .limit(1);

  return Boolean(row);
}

async function getQuota(userId: string, kind: WatchKind) {
  const [row] = await db
    .select({ tv: users.watchingNowTvSlots, movies: users.watchingNowMovieSlots })
    .from(users)
    .where(eq(users.id, userId))
    .limit(1);

  return kind === "movies" ? row?.movies ?? 10 : row?.tv ?? 5;
}

async function activeSlots(userId: string, mediaType: MediaType) {
  return db
    .select({ id: watchingNowItems.id, slotNumber: watchingNowItems.slotNumber })
    .from(watchingNowItems)
    .innerJoin(mediaItems, eq(mediaItems.id, watchingNowItems.mediaItemId))
    .where(
      and(
        eq(watchingNowItems.userId, userId),
        isNull(watchingNowItems.removedAt),
        eq(mediaItems.mediaType, mediaType),
      ),
    );
}

async function firstOpenSlot(userId: string, mediaType: MediaType, quota: number) {
  const occupied = new Set(
    (await activeSlots(userId, mediaType))
      .map((row) => row.slotNumber)
      .filter((slot): slot is number => typeof slot === "number"),
  );

  for (let slot = 1; slot <= quota; slot += 1) {
    if (!occupied.has(slot)) {
      return slot;
    }
  }

  return null;
}

async function clearSlot(userId: string, mediaType: MediaType, slotNumber: number, keepId?: string) {
  const rows = await db
    .select({ id: watchingNowItems.id })
    .from(watchingNowItems)
    .innerJoin(mediaItems, eq(mediaItems.id, watchingNowItems.mediaItemId))
    .where(
      and(
        eq(watchingNowItems.userId, userId),
        isNull(watchingNowItems.removedAt),
        eq(mediaItems.mediaType, mediaType),
        eq(watchingNowItems.slotNumber, slotNumber),
        keepId ? ne(watchingNowItems.id, keepId) : undefined,
      ),
    );

  await Promise.all(
    rows.map((row) =>
      db.update(watchingNowItems).set({ removedAt: new Date() }).where(eq(watchingNowItems.id, row.id)),
    ),
  );
}

export async function addToWatchNow(formData: FormData): Promise<WatchNowResult | undefined> {
  const session = await auth();
  if (!session) redirect("/login");

  const kind = optionalString(formData.get("kind"));
  const mediaType = mediaTypeFromKind(kind);
  const tmdbId = optionalString(formData.get("tmdbId"));
  const title = optionalString(formData.get("title"));

  if (!mediaType || !tmdbId || !title) {
    return { ok: false, message: "This title could not be added to Watch Now." };
  }

  const yearValue = optionalString(formData.get("year"));
  const parsedYear = yearValue ? Number.parseInt(yearValue, 10) : Number.NaN;
  const year = Number.isFinite(parsedYear) ? parsedYear : null;
  const classic = await matchingClassic(mediaType, tmdbId, title, year);

  if (classic) {
    return { ok: false, message: title + " is a Timeless Classic. It will always be available, so it does not need a Watch Now slot." };
  }

  const watchKind = kindFromMediaType(mediaType);
  const quota = await getQuota(session.user.id, watchKind);
  const requestedSlot = parseSlot(formData.get("slotNumber"));
  const targetSlot = requestedSlot && requestedSlot <= quota
    ? requestedSlot
    : await firstOpenSlot(session.user.id, mediaType, quota);

  if (!targetSlot) {
    revalidatePath("/");
    return { ok: false, message: "No Watch Now slots are open." };
  }
  const overview = optionalString(formData.get("overview"));
  const posterPath = optionalString(formData.get("posterPath"));
  const releaseDate = validDate(formData.get("releaseDate"));

  const existingId = await findMediaItemByTmdbId(mediaType, tmdbId);

  if (existingId && await mediaItemIsClassic(existingId)) {
    return { ok: false, message: title + " is a Timeless Classic. It will always be available, so it does not need a Watch Now slot." };
  }

  const mediaItemId = existingId ?? await ensureMediaItem({
    mediaType,
    tmdbId,
    title,
    year: Number.isFinite(year) ? year : null,
    overview,
    posterPath,
    releaseDate,
  });

  const [active] = await db
    .select({ id: watchingNowItems.id })
    .from(watchingNowItems)
    .where(
      and(
        eq(watchingNowItems.userId, session.user.id),
        eq(watchingNowItems.mediaItemId, mediaItemId),
        isNull(watchingNowItems.removedAt),
      ),
    )
    .limit(1);

  await clearSlot(session.user.id, mediaType, targetSlot, active?.id);

  if (active) {
    await db.update(watchingNowItems).set({ slotNumber: targetSlot }).where(eq(watchingNowItems.id, active.id));
  } else {
    await db.insert(watchingNowItems).values({
      userId: session.user.id,
      mediaItemId,
      scope: "show",
      slotNumber: targetSlot,
    });
  }

  revalidatePath("/");
  return { ok: true };
}

/**
 * Remove a title from Watch Now knowing only what the board knows.
 *
 * The catalog deals in TMDB ids -- most of what it shows has no row here at
 * all -- so the detail panel cannot name the watching_now row it wants gone.
 * Looking it up from the id is what lets one button both add and remove.
 */
export async function removeFromWatchNowByTmdbId(formData: FormData): Promise<WatchNowResult> {
  const session = await auth();
  if (!session) redirect("/login");

  const mediaType = mediaTypeFromKind(optionalString(formData.get("kind")));
  const tmdbId = optionalString(formData.get("tmdbId"));
  if (!mediaType || !tmdbId) {
    return { ok: false, message: "This title could not be identified." };
  }

  const mediaItemId = await findMediaItemByTmdbId(mediaType, tmdbId);
  // Nothing here means nothing to remove, which is the state the caller wanted.
  if (mediaItemId) {
    await db
      .update(watchingNowItems)
      .set({ removedAt: new Date() })
      .where(
        and(
          eq(watchingNowItems.userId, session.user.id),
          eq(watchingNowItems.mediaItemId, mediaItemId),
          isNull(watchingNowItems.removedAt),
        ),
      );
  }

  revalidatePath("/");
  return { ok: true };
}

export async function removeFromWatchNow(formData: FormData) {
  const session = await auth();
  if (!session) redirect("/login");

  const itemId = optionalString(formData.get("watchNowItemId"));
  if (!itemId) {
    return;
  }

  await db
    .update(watchingNowItems)
    .set({ removedAt: new Date() })
    .where(and(eq(watchingNowItems.id, itemId), eq(watchingNowItems.userId, session.user.id)));

  revalidatePath("/");
}

export async function moveWatchNowItem(formData: FormData) {
  const session = await auth();
  if (!session) redirect("/login");

  const itemId = optionalString(formData.get("watchNowItemId"));
  const targetSlot = parseSlot(formData.get("slotNumber"));

  if (!itemId || !targetSlot) {
    return;
  }

  const [item] = await db
    .select({ mediaType: mediaItems.mediaType })
    .from(watchingNowItems)
    .innerJoin(mediaItems, eq(mediaItems.id, watchingNowItems.mediaItemId))
    .where(
      and(
        eq(watchingNowItems.id, itemId),
        eq(watchingNowItems.userId, session.user.id),
        isNull(watchingNowItems.removedAt),
      ),
    )
    .limit(1);

  if (!item) {
    return;
  }

  const mediaType = item.mediaType;
  const quota = await getQuota(session.user.id, kindFromMediaType(mediaType));

  if (targetSlot > quota) {
    return;
  }

  await clearSlot(session.user.id, mediaType, targetSlot, itemId);
  await db.update(watchingNowItems).set({ slotNumber: targetSlot }).where(eq(watchingNowItems.id, itemId));

  revalidatePath("/");
}
