// Plex domain: browse/search/drill-down of the mirrored catalog, the admin collection editor,
// Siftlode-native Plex playlists, HLS/direct playback sessions + progress mirroring, and the
// two-way watch-sync link. Self-contained — its types reference only each other, and its methods
// touch only the shared `req`/`beacon` from core plus the local `appendPlexFilters` helper.
import { req, beacon } from "./core";

// Admin: result of testing the Plex server connection (also drives the library-picker).
interface PlexSection {
  key: string;
  title: string;
  type: string; // "movie" | "show"
  count?: number;
}
export interface PlexTestResult {
  ok: boolean;
  server_name: string;
  version?: string;
  sections: PlexSection[];
  // Whether a sample media file resolved + was readable through the local mount (bind-mount +
  // path-map check). checked=false when the library is empty (nothing to probe yet).
  media_check?: {
    checked: boolean;
    ok?: boolean;
    plex_path?: string;
    local_path?: string;
  };
}

// Plex browse/search/drill-down (the mirrored catalog rendered as feed-style cards).
export interface PlexLibrary {
  key: string;
  title: string;
  kind: "movie" | "show";
  count: number;
}
export interface PlexCard {
  id: string;
  type: "movie" | "show" | "episode";
  title: string;
  year?: number | null;
  duration_seconds?: number | null;
  thumb: string;
  playable?: string; // direct | remux | transcode
  status?: string; // new | watched | hidden
  position_seconds?: number;
  season_count?: number | null; // show
  season_number?: number | null; // episode
  episode_number?: number | null; // episode
  show_title?: string | null; // episode (in a mixed playlist)
  show_id?: string | null; // episode: the show's rating_key (for grouping a playlist by show/season)
  summary?: string | null; // episode
}
// Unified cross-library browse (movies + shows mixed). `episodes` is populated only on a search that
// also matches episodes (the grouped "Episodes" section).
export interface PlexUnifiedResult {
  scope: string; // movie | show | both
  total: number;
  offset: number;
  limit: number;
  items: PlexCard[];
  episodes: PlexCard[];
}
export interface PlexSeasonDetail {
  id: string; // season rating_key
  season_number: number | null;
  title: string;
  thumb: string;
  episode_count: number;
  status: string; // aggregate: new | in_progress | watched
  resume?: PlexCard | null; // on-deck episode (last in-progress, else first unwatched)
  first?: PlexCard | null; // first episode (Play from the start)
  episodes: PlexCard[];
}
export interface PlexShowDetail {
  show: {
    id: string;
    title: string;
    summary?: string | null;
    year?: number | null;
    thumb: string;
    art: string;
    library?: string | null; // Plex section key (for the admin collection editor)
    content_rating?: string | null;
    rating?: number | null;
    genres: string[];
    studio?: string | null;
    season_count?: number | null;
    imdb_rating?: number | null;
    imdb_id?: string | null;
    imdb_url?: string | null;
    cast: PlexCastMember[];
    status: string; // aggregate across all episodes
    resume?: PlexCard | null;
    first?: PlexCard | null;
    episode_count: number;
    collection_keys: string[];
  };
  seasons: PlexSeasonDetail[];
  related: PlexCard[];
}

export interface PlexMarker {
  type: "intro" | "credits";
  start_s: number;
  end_s: number;
}
export interface PlexItemDetail {
  id: string;
  kind: "movie" | "episode";
  title: string;
  summary?: string | null;
  year?: number | null;
  duration_seconds: number | null;
  playable: string; // direct | remux | transcode
  thumb: string;
  art: string;
  library?: string | null; // Plex section key (for the admin collection editor)
  cast: PlexCastMember[];
  imdb_rating?: number | null;
  imdb_id?: string | null;
  imdb_url?: string | null;
  content_rating?: string | null;
  genres: string[];
  directors: string[];
  studio?: string | null;
  tagline?: string | null;
  markers: PlexMarker[];
  audio_streams: { ord: number; label: string; language?: string | null; default: boolean }[];
  // `text`=true → offered as a WebVTT <track>; image subs (PGS/VobSub) are text=false (hidden).
  subtitle_streams: {
    ord: number;
    label: string;
    language?: string | null;
    codec?: string;
    text: boolean;
  }[];
  status: string;
  position_seconds: number;
  show_title?: string | null;
  season_number?: number | null;
  episode_number?: number | null;
  prev_id?: string | null;
  next_id?: string | null;
  // `source` buckets the strip so each type can be shown/hidden independently on the info page:
  // "collection" (genuine franchise), "imdb"/"tmdb"/"tvdb"/"trakt" (external auto-lists), "smart".
  collections?: { id: string; title: string; source: string; items: PlexCard[] }[];
}
export interface PlexCollection {
  id: string;
  title: string;
  summary?: string | null;
  thumb?: string | null;
  child_count?: number | null;
  smart: boolean;
  source?: string;
  editable: boolean;
  can_edit?: boolean; // effective: admin-marked editable AND a plain (non-smart/non-auto) collection
}
export interface PlexPlaylist {
  id: number;
  title: string;
  item_count: number;
  thumb?: string | null;
  has_item?: boolean; // only when the list was fetched with ?contains=<rating_key>
  group_in?: number; // only when fetched with ?contains_group=<rk,rk,…>: how many of the group it holds
}
export interface PlexPlaylistDetail {
  id: number;
  title: string;
  items: PlexCard[];
}
export interface PlexCastMember {
  name: string;
  role?: string | null;
  thumb?: string | null; // proxied person-image url, or null when Plex has no photo
}
export interface PlexPersonCard {
  name: string;
  photo?: string | null; // proxied person-image url, or null (placeholder)
  count: number; // in-scope movies/shows featuring them
}
// The expanded Plex browse filters (movie libraries). Persisted per-account as JSON.
export interface PlexFilters {
  genres: string[];
  genreMode?: "any" | "all"; // how multiple genres combine (default any)
  contentRatings: string[];
  yearMin?: number | null;
  yearMax?: number | null;
  ratingMin?: number | null;
  durationMin?: number | null; // seconds
  durationMax?: number | null;
  addedWithin?: string; // "" | "24h" | "1w" | "1m" | "6m" | "1y"
  directors: string[]; // AND — titles featuring ALL selected
  actors: string[]; // OR (any) by default, or AND (all) via actorMode
  actorMode?: "any" | "all"; // how multiple actors combine (default any = OR)
  studios: string[]; // OR — from any selected studio
  collection?: string | null; // a single Plex collection (rating_key)
  collectionTitle?: string | null; // its title, for the active-filter chip
  sortDir?: "asc" | "desc"; // sort direction (default desc)
}
export const EMPTY_PLEX_FILTERS: PlexFilters = {
  genres: [],
  contentRatings: [],
  directors: [],
  actors: [],
  studios: [],
};
export function plexFilterCount(f: PlexFilters): number {
  return (
    f.genres.length +
    f.contentRatings.length +
    (f.yearMin != null || f.yearMax != null ? 1 : 0) +
    (f.ratingMin != null ? 1 : 0) +
    (f.durationMin != null || f.durationMax != null ? 1 : 0) +
    (f.addedWithin ? 1 : 0) +
    f.directors.length +
    f.actors.length +
    f.studios.length +
    (f.collection ? 1 : 0)
  );
}
// Serialize the shared PlexFilters metadata fields onto a query string. Used by both the library grid
// and the facets request so the two endpoints always agree on the filter set (paging/sort/sort_dir are
// each caller's own concern and stay out of here).
function appendPlexFilters(u: URLSearchParams, f: PlexFilters): void {
  if (f.genres?.length) u.set("genres", f.genres.join(","));
  if (f.genreMode === "all") u.set("genre_mode", "all");
  if (f.contentRatings?.length) u.set("content_ratings", f.contentRatings.join(","));
  if (f.yearMin != null) u.set("year_min", String(f.yearMin));
  if (f.yearMax != null) u.set("year_max", String(f.yearMax));
  if (f.ratingMin != null) u.set("rating_min", String(f.ratingMin));
  if (f.durationMin != null) u.set("duration_min", String(f.durationMin));
  if (f.durationMax != null) u.set("duration_max", String(f.durationMax));
  if (f.addedWithin) u.set("added_within", f.addedWithin);
  if (f.directors?.length) u.set("directors", f.directors.join(","));
  if (f.actors?.length) u.set("actors", f.actors.join(","));
  if (f.actorMode === "all") u.set("actor_mode", "all");
  if (f.studios?.length) u.set("studios", f.studios.join(","));
  if (f.collection) u.set("collection", f.collection);
}
export interface PlexFacets {
  genres: { value: string; count: number }[];
  content_ratings: { value: string; count: number }[];
  year_min: number | null;
  year_max: number | null;
  rating_max: number | null;
  duration_min: number | null;
  duration_max: number | null;
}
export interface PlexPlaySession {
  mode: "direct" | "hls";
  url: string;
  start_s: number;
}

// Two-way Plex watch-state sync link status (owner account; Phase A).
export interface PlexWatchLink {
  linked: boolean;
  sync_enabled: boolean;
  uses_admin: boolean;
  initial_import_done: boolean;
  last_watch_sync_at: string | null;
}
export interface PlexWatchImport {
  watched: number;
  in_progress: number;
  scanned: number;
}

export const plexApi = {
  testPlex: (): Promise<PlexTestResult> => req("/api/plex/test", { method: "POST" }),
  syncPlex: (): Promise<Record<string, unknown>> => req("/api/plex/sync", { method: "POST" }),
  plexLibraries: (): Promise<{ enabled: boolean; libraries: PlexLibrary[] }> =>
    req("/api/plex/libraries"),
  plexLibrary: (p: {
    scope: string; // movie | show | both
    q?: string;
    sort?: string;
    show?: string;
    offset?: number;
    limit?: number;
    filters?: PlexFilters;
  }): Promise<PlexUnifiedResult> => {
    const u = new URLSearchParams({ scope: p.scope });
    if (p.q) u.set("q", p.q);
    if (p.sort) u.set("sort", p.sort);
    if (p.show && p.show !== "all") u.set("show", p.show);
    if (p.offset) u.set("offset", String(p.offset));
    if (p.limit) u.set("limit", String(p.limit));
    const f = p.filters;
    if (f) {
      appendPlexFilters(u, f);
      u.set("sort_dir", f.sortDir ?? "asc"); // default ascending
    }
    return req(`/api/plex/library?${u.toString()}`);
  },
  // Facets ADAPT to the active filters + watch-state (faceted search) — pass them so each group
  // narrows the others and the bounds match the visible result set.
  plexFacets: (scope: string, f?: PlexFilters, show?: string): Promise<PlexFacets> => {
    const u = new URLSearchParams({ scope });
    if (show && show !== "all") u.set("show", show);
    if (f) appendPlexFilters(u, f);
    return req(`/api/plex/facets?${u.toString()}`);
  },
  // With a library plex_key → that library's collections (editor). Without one → the union across all
  // enabled libraries (the unified-scope sidebar picker).
  plexCollections: (library?: string, q?: string): Promise<{ collections: PlexCollection[] }> => {
    const u = new URLSearchParams();
    if (library) u.set("library", library);
    if (q) u.set("q", q);
    const qs = u.toString();
    return req(`/api/plex/collections${qs ? `?${qs}` : ""}`);
  },
  // --- Collection editing (admin only; writes back to Plex) ---
  plexCreateCollection: (
    library: string,
    title: string,
    item_rating_key: string,
  ): Promise<PlexCollection> =>
    req(`/api/plex/collections`, {
      method: "POST",
      body: JSON.stringify({ library, title, item_rating_key }),
    }),
  plexCollectionAddItem: (rk: string, itemRk: string): Promise<PlexCollection> =>
    req(`/api/plex/collections/${encodeURIComponent(rk)}/items/${encodeURIComponent(itemRk)}`, {
      method: "POST",
    }),
  plexCollectionRemoveItem: (rk: string, itemRk: string): Promise<PlexCollection> =>
    req(`/api/plex/collections/${encodeURIComponent(rk)}/items/${encodeURIComponent(itemRk)}`, {
      method: "DELETE",
    }),
  plexRenameCollection: (rk: string, title: string): Promise<PlexCollection> =>
    req(`/api/plex/collections/${encodeURIComponent(rk)}`, {
      method: "PATCH",
      body: JSON.stringify({ title }),
    }),
  plexDeleteCollection: (rk: string): Promise<{ deleted: string }> =>
    req(`/api/plex/collections/${encodeURIComponent(rk)}`, { method: "DELETE" }),
  plexSetCollectionEditable: (rk: string, editable: boolean): Promise<PlexCollection> =>
    req(`/api/plex/collections/${encodeURIComponent(rk)}/editable`, {
      method: "POST",
      body: JSON.stringify({ editable }),
    }),
  // --- Playlists (Siftlode-native, per-user, ordered) ---
  // `contains`=single rating_key → per-playlist has_item; `containsGroup`=a set of rating_keys (a whole
  // season/show) → per-playlist group_in + a top-level group_size (for the bulk add-to-playlist dialog).
  plexPlaylists: (
    contains?: string,
    containsGroup?: string[],
  ): Promise<{ playlists: PlexPlaylist[]; group_size?: number }> => {
    const p = new URLSearchParams();
    if (contains) p.set("contains", contains);
    if (containsGroup) p.set("contains_group", containsGroup.join(","));
    const qs = p.toString();
    return req(`/api/plex/playlists${qs ? `?${qs}` : ""}`);
  },
  plexPlaylist: (id: number): Promise<PlexPlaylistDetail> => req(`/api/plex/playlists/${id}`),
  plexCreatePlaylist: (title: string, item_rating_key?: string): Promise<PlexPlaylist> =>
    req(`/api/plex/playlists`, {
      method: "POST",
      body: JSON.stringify({ title, item_rating_key }),
    }),
  plexPlaylistAddBulk: (
    id: number,
    itemRks: string[],
  ): Promise<{ added: number; item_count: number }> =>
    req(`/api/plex/playlists/${id}/items/bulk`, {
      method: "POST",
      body: JSON.stringify({ item_rating_keys: itemRks }),
    }),
  plexPlaylistRemoveBulk: (
    id: number,
    itemRks: string[],
  ): Promise<{ removed: number; item_count: number }> =>
    req(`/api/plex/playlists/${id}/items/remove-bulk`, {
      method: "POST",
      body: JSON.stringify({ item_rating_keys: itemRks }),
    }),
  plexRenamePlaylist: (id: number, title: string): Promise<PlexPlaylist> =>
    req(`/api/plex/playlists/${id}`, { method: "PATCH", body: JSON.stringify({ title }) }),
  plexDeletePlaylist: (id: number): Promise<{ deleted: number }> =>
    req(`/api/plex/playlists/${id}`, { method: "DELETE" }),
  plexPlaylistAddItem: (id: number, itemRk: string): Promise<{ ok: boolean }> =>
    req(`/api/plex/playlists/${id}/items`, {
      method: "POST",
      body: JSON.stringify({ item_rating_key: itemRk }),
    }),
  plexPlaylistRemoveItem: (id: number, itemRk: string): Promise<{ ok: boolean }> =>
    req(`/api/plex/playlists/${id}/items/${encodeURIComponent(itemRk)}`, { method: "DELETE" }),
  plexReorderPlaylist: (id: number, itemRks: string[]): Promise<{ ok: boolean }> =>
    req(`/api/plex/playlists/${id}/order`, {
      method: "PUT",
      body: JSON.stringify({ item_rating_keys: itemRks }),
    }),
  plexShow: (id: string): Promise<PlexShowDetail> =>
    req(`/api/plex/show/${encodeURIComponent(id)}`),
  // Mark a whole show / season watched or unwatched (all its episodes) for the current user.
  plexShowState: (rk: string, watched: boolean): Promise<{ changed: number; watched: boolean }> =>
    req(`/api/plex/show/${encodeURIComponent(rk)}/state`, {
      method: "POST",
      body: JSON.stringify({ watched }),
    }),
  plexSeasonState: (rk: string, watched: boolean): Promise<{ changed: number; watched: boolean }> =>
    req(`/api/plex/season/${encodeURIComponent(rk)}/state`, {
      method: "POST",
      body: JSON.stringify({ watched }),
    }),
  plexItem: (id: string): Promise<PlexItemDetail> =>
    req(`/api/plex/item/${encodeURIComponent(id)}`),
  // Cards for the actor filter-chips currently applied: name + cached headshot + in-scope film/show count.
  plexPeopleCards: (names: string[], scope: string): Promise<{ people: PlexPersonCard[] }> =>
    req(
      `/api/plex/people/cards?scope=${encodeURIComponent(scope)}&names=${encodeURIComponent(names.join(","))}`,
    ),
  plexSession: (
    id: string,
    start = 0,
    audio?: number | null,
    aoff = 0,
    multi = false,
  ): Promise<PlexPlaySession> => {
    const p = new URLSearchParams({ start: String(Math.max(0, Math.floor(start))) });
    if (audio != null) p.set("audio", String(audio));
    if (aoff) p.set("aoff", String(aoff));
    if (multi) p.set("multi", "1"); // ≥2 audio tracks → multi-rendition HLS (client-side audio switch)
    return req(`/api/plex/stream/${encodeURIComponent(id)}/session?${p.toString()}`, {
      method: "POST",
    });
  },
  // WebVTT URL for a text subtitle track (used directly as a <track src>). Same-origin → cookie-authed.
  // `offset` = the session's real media start (0 for direct-play); the backend shifts the absolute
  // cue times onto the HLS session's zero-based clock so subtitles line up with speech after a seek.
  plexSubtitleUrl: (id: string, ord: number, offset = 0): string =>
    `/api/plex/subtitle/${encodeURIComponent(id)}/${ord}${offset ? `?offset=${offset}` : ""}`,
  // `final` marks a settled checkpoint (pause / page-hide / leaving the player) as opposed to the
  // periodic 10s tick — the backend mirrors the resume position to a linked Plex account only on a
  // final checkpoint, so a single watch doesn't spray Plex with timeline updates.
  plexProgress: (
    id: string,
    position_seconds: number,
    duration_seconds: number,
    final = false,
  ): Promise<unknown> =>
    req(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
      method: "POST",
      body: JSON.stringify({ position_seconds, duration_seconds, final }),
    }),
  // Fire-and-forget progress save that SURVIVES page unload (F5/close/navigate): a normal fetch is
  // cancelled on unload and React effect cleanup doesn't run on a full reload, so the last position
  // (esp. right after a seek) would be lost. `keepalive` lets the POST complete during unload; we
  // replicate req()'s credentials + account header (no retry — there's no page left to retry on).
  plexProgressBeacon: (
    id: string,
    position_seconds: number,
    duration_seconds: number,
    final = false,
  ): void =>
    beacon(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
      position_seconds,
      duration_seconds,
      final,
    }),
  // Inline union (matches api.ts's VideoStatus by design) rather than importing VideoStatus — that
  // lives in the api.ts barrel, which imports this module, so reusing it would make a type cycle.
  plexSetState: (id: string, status: "new" | "watched" | "hidden"): Promise<unknown> =>
    req(`/api/plex/item/${encodeURIComponent(id)}/state`, {
      method: "POST",
      body: JSON.stringify({ status }),
    }),
  // Two-way Plex watch-sync (Phase A: owner link + one-time "Plex is master" import).
  plexWatchLink: (): Promise<PlexWatchLink> => req("/api/plex/watch/link"),
  plexWatchSetLink: (
    enabled: boolean,
  ): Promise<PlexWatchLink & { import: PlexWatchImport | null }> =>
    req("/api/plex/watch/link", { method: "POST", body: JSON.stringify({ enabled }) }),
  plexWatchImport: (): Promise<PlexWatchLink & { import: PlexWatchImport }> =>
    req("/api/plex/watch/import", { method: "POST" }),
  plexStreamFileUrl: (id: string): string => `/api/plex/stream/${encodeURIComponent(id)}/file`,
  plexDownloadUrl: (id: string): string =>
    `/api/plex/stream/${encodeURIComponent(id)}/file?download=1`,
  plexImageUrl: (id: string, variant?: "thumb" | "art"): string =>
    `/api/plex/image/${encodeURIComponent(id)}${variant === "art" ? "?variant=art" : ""}`,
};
