// Feed, video watch-state/progress, and live YouTube search. `filterParams`/`feedQuery` translate
// the shared FeedFilters into the query string the feed + facet endpoints both consume; they stay
// module-local (no consumer names them). Only touches core's req/beacon.
import { req, beacon } from "./core";

// Per-user watch status stored server-side (VALID_STATES in routes/feed.py). "in_progress" and
// "unwatched" are NOT statuses — they're derived feed filters (status "new" + a resume position),
// so they never appear on the wire.
export type VideoStatus = "new" | "watched" | "hidden";

export interface Video {
  id: string;
  title: string | null;
  channel_id: string;
  channel_title: string | null;
  channel_thumbnail: string | null;
  channel_url: string;
  published_at: string | null;
  thumbnail_url: string | null;
  duration_seconds: number | null;
  view_count: number | null;
  is_short: boolean;
  live_status: string;
  status: VideoStatus;
  position_seconds: number;
  saved: boolean; // is the video in the user's built-in Watch later playlist
  watch_url: string;
}

export interface VideoDetail {
  id: string;
  description: string | null;
  like_count: number | null;
  in_db: boolean;
  channel_id: string | null;
  channel_title: string | null;
  published_at: string | null;
  view_count: number | null;
  duration_seconds: number | null;
}

export interface FeedResponse {
  items: Video[];
  has_more: boolean;
  // Opaque keyset cursor for the next page, or null when this is the last page.
  next_cursor: string | null;
  // Only set by the live YouTube search: "scrape" (InnerTube, no API quota) or "api"
  // (search.list, 100 units/page) — lets the UI drop the quota warning in scrape mode.
  source?: "scrape" | "api";
  // NOTE: /api/feed also returns an echoed `limit`, but /api/search/youtube does not and nothing
  // in the app reads it, so it's deliberately omitted rather than typed required (a lie for search)
  // or optional (dead surface). Re-add if a consumer ever needs it.
}

export interface FeedFilters {
  tags: number[];
  tagMode: "or" | "and";
  q: string;
  sort: string;
  // Re-roll token for the "shuffle" sort; bumped by the reshuffle button so the
  // feed re-queries with a fresh order. Ignored by every other sort.
  seed?: number;
  // "my" = only your subscriptions (default); "all" = the whole shared catalog.
  scope: "my" | "all";
  includeNormal: boolean;
  includeShorts: boolean;
  includeLive: boolean;
  show: string;
  // Library (scope "all") only — provenance filter: "organic" (default) hides search-
  // discovered videos, "all" mixes them in, "search" shows only them. Ignored for "my".
  librarySource?: "organic" | "all" | "search" | "plex";
  /** OR-ed together: a video from ANY of these passes, and every other filter still applies. */
  channelIds: string[];
  maxAgeDays?: number;
  minDuration?: number;
  maxDuration?: number;
  dateFrom?: string;
  dateTo?: string;
}

// The filter half of the query (everything except paging/sort), shared by the feed and
// the facet-count endpoint so both see exactly the same filter context.
function filterParams(f: FeedFilters): URLSearchParams {
  const p = new URLSearchParams();
  f.tags.forEach((t) => p.append("tags", String(t)));
  p.set("tag_mode", f.tagMode);
  if (f.q) p.set("q", f.q);
  p.set("scope", f.scope);
  p.set("show_normal", String(f.includeNormal));
  p.set("include_shorts", String(f.includeShorts));
  p.set("include_live", String(f.includeLive));
  p.set("show", f.show);
  f.channelIds.forEach((id) => p.append("channel_ids", id));
  if (f.maxAgeDays) p.set("max_age_days", String(f.maxAgeDays));
  if (f.dateFrom) p.set("published_after", f.dateFrom);
  if (f.dateTo) p.set("published_before", f.dateTo);
  if (f.minDuration != null) p.set("min_duration", String(f.minDuration));
  if (f.maxDuration != null) p.set("max_duration", String(f.maxDuration));
  // Only relevant in Library scope; the backend ignores it for "my". Default = hide search.
  p.set("library_source", f.librarySource ?? "organic");
  return p;
}

function feedQuery(f: FeedFilters, cursor: string | null, limit: number): string {
  const p = filterParams(f);
  p.set("sort", f.sort);
  if (f.sort === "shuffle" && f.seed) p.set("seed", String(f.seed));
  if (cursor) p.set("cursor", cursor);
  p.set("limit", String(limit));
  return p.toString();
}

export const feedApi = {
  feed: (f: FeedFilters, cursor: string | null, limit: number): Promise<FeedResponse> =>
    req(`/api/feed?${feedQuery(f, cursor, limit)}`),
  feedCount: (f: FeedFilters): Promise<{ count: number }> =>
    req(`/api/feed/count?${filterParams(f).toString()}`),
  // Live YouTube search: results are materialised into the catalog and returned as feed cards.
  // `quiet` so the YT-search panel can render quota/limit errors (incl. 429) inline instead of
  // popping the global modal. `cursor` is the next-page token (an InnerTube continuation token
  // in scrape mode, or a Data API pageToken in api mode); the response's `source` says which.
  searchYoutube: (q: string, cursor: string | null, limit?: number): Promise<FeedResponse> => {
    const p = new URLSearchParams({ q });
    if (cursor) p.set("cursor", cursor);
    if (limit) p.set("limit", String(limit));
    return req(`/api/search/youtube?${p.toString()}`, {}, { quiet: true });
  },
  // Discard a set of live-search results "as if never added" (drops your search-finds + deletes
  // the now-orphaned, un-kept videos). Returns how many videos were removed.
  clearSearch: (videoIds: string[]): Promise<{ removed: number }> =>
    req("/api/search/clear", { method: "POST", body: JSON.stringify({ video_ids: videoIds }) }),
  facets: (f: FeedFilters): Promise<{ counts: Record<string, number> }> =>
    req(`/api/facets?${filterParams(f).toString()}`),
  // idempotent: setting a state / saving a progress checkpoint is safe to replay, so these
  // recover from a transient gateway/keepalive 502 instead of surfacing it (see req()).
  setState: (id: string, status: string) =>
    req(
      `/api/videos/${id}/state`,
      { method: "POST", body: JSON.stringify({ status }) },
      { idempotent: true },
    ),
  clearState: (id: string) =>
    req(`/api/videos/${id}/state`, { method: "DELETE" }, { idempotent: true }),
  saveProgress: (id: string, positionSeconds: number, durationSeconds: number) =>
    req(
      `/api/videos/${id}/progress`,
      {
        method: "POST",
        body: JSON.stringify({
          position_seconds: Math.floor(positionSeconds),
          duration_seconds: Math.floor(durationSeconds),
        }),
      },
      { idempotent: true },
    ),
  // Fire-and-forget YT progress save that survives page unload (F5/close/navigate).
  saveProgressBeacon: (id: string, positionSeconds: number, durationSeconds: number): void =>
    beacon(`/api/videos/${encodeURIComponent(id)}/progress`, {
      position_seconds: Math.floor(positionSeconds),
      duration_seconds: Math.floor(durationSeconds),
    }),
  videoDetail: (id: string): Promise<VideoDetail> => req(`/api/videos/${id}`),
};
