// Pure logic for the "More Episodes" strip in EpisodeFocusView.
//
// Extracted from the component so it can be unit-tested: the strip must never
// collapse to just the current episode while real siblings exist, must not
// mistake number-less episodes for the current one, and must run past a season
// boundary rather than dead-ending at the end of a season (ux-flows §5B.2).
//
// TRACES: UR-048, UR-062 | DR-062, DR-104
import type { MediaItem } from "$lib/api/types";

/** Episodes shown before / after the current one in the strip window. */
const BEFORE = 3;
const AFTER = 6;

/** Jellyfin puts specials in season 0; they air outside the numbered run. */
const SPECIALS_SEASON = 0;

/**
 * Does `ep` refer to the same episode as `current`?
 *
 * Matches by id first. Falls back to season+episode number, but ONLY when both
 * numbers are known on both sides — otherwise `undefined === undefined` would
 * mark every number-less episode as the current one (the bug that made the
 * whole strip look like the current episode).
 */
export function isCurrentEpisode(ep: MediaItem, current: MediaItem): boolean {
  if (ep.id === current.id) return true;
  if (
    ep.indexNumber == null || current.indexNumber == null ||
    ep.parentIndexNumber == null || current.parentIndexNumber == null
  ) {
    return false;
  }
  return (
    ep.parentIndexNumber === current.parentIndexNumber &&
    ep.indexNumber === current.indexNumber
  );
}

/**
 * Sort key for a season: specials (season 0) come *after* every numbered
 * season, matching how a viewer works through a show — S1, S2, …, then the
 * extras — rather than opening on a special because 0 < 1.
 */
function seasonRank(seasonNumber: number | null | undefined): number {
  return seasonNumber === SPECIALS_SEASON ? Number.MAX_SAFE_INTEGER : seasonNumber!;
}

/**
 * Broadcast order across a whole series: season ascending, then episode.
 *
 * When *either* side's season is unknown there is no season axis to compare on,
 * so it falls through to episode number. That makes the comparator technically
 * non-transitive across such a mix, which is safe here because only the
 * directly-fetched `current` episode can lack a season and it is never part of
 * the array being sorted — it is only positioned against it (see
 * `adjacentEpisodes`).
 */
export function compareSeriesOrder(a: MediaItem, b: MediaItem): number {
  if (a.parentIndexNumber != null && b.parentIndexNumber != null) {
    const bySeason = seasonRank(a.parentIndexNumber) - seasonRank(b.parentIndexNumber);
    if (bySeason !== 0) return bySeason;
  }
  return (a.indexNumber ?? 0) - (b.indexNumber ?? 0);
}

/**
 * The window of episodes shown under the hero: up to 3 before and 6 after the
 * current episode, in series order across *all* seasons.
 *
 * Crossing a season boundary is the point (ux-flows §5B.2): finishing a season
 * finale should offer the next season's premiere, not an empty strip. Degrades
 * gracefully:
 *  - splices the current episode into the pool at its ordered position when it
 *    isn't present (an API id mismatch on a directly-fetched episode), so it
 *    still anchors the window;
 *  - returns just `[current]` only when there genuinely are no other episodes.
 */
export function adjacentEpisodes(current: MediaItem, allEpisodes: MediaItem[]): MediaItem[] {
  const pool = allEpisodes.slice().sort(compareSeriesOrder);

  let idx = pool.findIndex((e) => isCurrentEpisode(e, current));

  if (idx === -1) {
    const insertAt = pool.findIndex((e) => compareSeriesOrder(e, current) > 0);
    idx = insertAt === -1 ? pool.length : insertAt;
    pool.splice(idx, 0, current);
  }

  const start = Math.max(0, idx - BEFORE);
  const end = Math.min(pool.length, idx + AFTER + 1);
  return pool.slice(start, end);
}

/**
 * Label for a strip card, relative to the episode in focus.
 *
 * Within the current season a bare number reads cleanly ("6."). Once the window
 * crosses into another season that number is ambiguous, so the card names the
 * season too ("S3E1") — otherwise the premiere after a finale just reads "1."
 */
export function stripCardLabel(ep: MediaItem, current: MediaItem): string {
  const crossesSeason =
    ep.parentIndexNumber != null &&
    current.parentIndexNumber != null &&
    ep.parentIndexNumber !== current.parentIndexNumber;

  if (crossesSeason) return `S${ep.parentIndexNumber}E${ep.indexNumber ?? 0}`;
  return `${ep.indexNumber ?? 0}.`;
}
