// Turning a TMDB catalog entry into a row in the collection.
//
// The public board is a TMDB browser: almost everything on it is a title
// Ampelos has never heard of, so every action that expresses an opinion about
// one -- Watch Now, watchlist, timeless -- has to be able to bring it into
// being first. That was inline in the Watch Now action and is shared now
// because a second copy of it would drift, and the two copies disagreeing about
// what a new media item looks like is a bug nobody would see until the fetcher
// went looking for a series with no `series` row.
//
// Identity only. Overview and poster come along because the board already has
// them and a title with neither reads as broken in the admin lists, but
// runtime, release date, and a series' season and episode list are
// refresh-metadata's job -- and it trusts the TMDB id written here over any
// title search, which is what keeps a created row pointing at what the person
// actually clicked.

import { and, eq } from "drizzle-orm";

import { db } from "@/db/client";
import { externalIds, mediaItems, movies, series } from "@/db/schema";

export type MediaType = "tv_series" | "movie";

export type CatalogRef = {
  mediaType: MediaType;
  tmdbId: string;
  title: string;
  year: number | null;
  overview?: string | null;
  posterPath?: string | null;
  releaseDate?: string | null;
};

/** The media item for a TMDB id, or null if the collection has never seen it. */
export async function findMediaItemByTmdbId(mediaType: MediaType, tmdbId: string) {
  const [row] = await db
    .select({ mediaItemId: externalIds.mediaItemId })
    .from(externalIds)
    .innerJoin(mediaItems, eq(mediaItems.id, externalIds.mediaItemId))
    .where(
      and(
        eq(externalIds.source, "tmdb"),
        eq(externalIds.externalId, tmdbId),
        eq(mediaItems.mediaType, mediaType),
      ),
    )
    .limit(1);

  return row?.mediaItemId ?? null;
}

/**
 * The media item for this catalog entry, creating it if it does not exist.
 *
 * The media type is part of the lookup because TMDB numbers films and series in
 * separate spaces: id 1399 is both a film and a series, and matching on the
 * number alone would hand somebody's film the series' row.
 */
export async function ensureMediaItem(ref: CatalogRef) {
  const existing = await findMediaItemByTmdbId(ref.mediaType, ref.tmdbId);
  if (existing) return existing;

  const [created] = await db
    .insert(mediaItems)
    .values({
      mediaType: ref.mediaType,
      title: ref.title,
      sortTitle: ref.title.toLocaleLowerCase("en-US"),
      overview: ref.overview ?? null,
      year: ref.year,
      posterPath: ref.posterPath ?? null,
    })
    .returning({ id: mediaItems.id });

  if (ref.mediaType === "movie") {
    await db.insert(movies).values({ id: created.id, releaseDate: ref.releaseDate ?? null });
  } else {
    await db.insert(series).values({ id: created.id, firstAirDate: ref.releaseDate ?? null });
  }

  // Verified on arrival. Everywhere else the TMDB id is a guess made by
  // matching a folder name against search results; here somebody clicked a
  // specific TMDB entry and asked for THAT. The id is the request, not an
  // inference about it, so there is nothing left for a human to check.
  //
  // ON CONFLICT DO NOTHING, because the unique index is on (source, id) alone
  // and losing that race must not fail the action the person actually asked
  // for. The row is still found by the lookup above on the next attempt.
  await db
    .insert(externalIds)
    .values({ mediaItemId: created.id, source: "tmdb", externalId: ref.tmdbId, verifiedAt: new Date() })
    .onConflictDoNothing();

  return created.id;
}
