import type { HomeResponse, ItemsResponse, JellyItem, PlaybackPlan } from './shared';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useClient } from 'hooks/useClient';

// Reads for the /jellyfin panels. Everything goes through /api/jellyfin/_officer/*, the auth proxy in front
// of the officer-jellyfin sidecar — the browser never learns the Jellyfin URL or its access token.
//
// One query key namespace, ['jellyfin'], and every mutation invalidates all of it. That is coarse on purpose:
// marking an episode watched changes Continue Watching, Next Up, the season list and the item itself, and
// enumerating those relationships would be four chances to forget one.

const KEY = 'jellyfin';

const query = (params: Record<string, string | number | boolean | null | undefined>): string => {
  const search = new URLSearchParams();
  for (const [key, value] of Object.entries(params)) {
    if (value !== undefined && value !== null && value !== '') search.set(key, String(value));
  }
  const qs = search.toString();
  return qs ? `?${qs}` : '';
};

/** Libraries, Continue Watching, Next Up and the newest thing per library — one round trip. */
export function useJellyfinHome(enabled = true) {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'home'],
    queryFn: () => get<HomeResponse>('/jellyfin/_officer/home'),
    enabled,
    staleTime: 30_000,
  });
}

/** Just the libraries — what the nav lists. Cheap enough to keep separate from the whole home payload. */
export function useJellyfinViews(enabled = true) {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'views'],
    queryFn: () => get<{ views: JellyItem[] }>('/jellyfin/_officer/views'),
    enabled,
    staleTime: 5 * 60_000,
  });
}

export type BrowseFilter = {
  parentId?: string;
  includeItemTypes?: string;
  sortBy?: string;
  sortOrder?: 'Ascending' | 'Descending';
  startIndex?: number;
  limit?: number;
  filters?: string;
  isFavorite?: boolean;
  recursive?: boolean;
  collapseBoxSetItems?: boolean;
  searchTerm?: string;
};

export function useJellyfinItems(filter: BrowseFilter, enabled = true) {
  const { get } = useClient();
  const qs = query(filter);
  return useQuery({
    queryKey: [KEY, 'items', qs],
    queryFn: () => get<ItemsResponse>(`/jellyfin/_officer/items${qs}`),
    enabled,
    staleTime: 30_000,
  });
}

export function useJellyfinItem(id: string | null) {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'item', id],
    queryFn: () => get<JellyItem>(`/jellyfin/_officer/items/${id}`),
    enabled: !!id,
    staleTime: 30_000,
  });
}

export function useSeasons(seriesId: string | null) {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'seasons', seriesId],
    queryFn: () => get<ItemsResponse>(`/jellyfin/_officer/shows/${seriesId}/seasons`),
    enabled: !!seriesId,
    staleTime: 60_000,
  });
}

export function useEpisodes(seriesId: string | null, seasonId: string | null) {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'episodes', seriesId, seasonId],
    queryFn: () => get<ItemsResponse>(`/jellyfin/_officer/shows/${seriesId}/episodes${query({ seasonId })}`),
    enabled: !!seriesId && !!seasonId,
    staleTime: 60_000,
  });
}

export function useJellyfinSearch(term: string, enabled = true) {
  const { get } = useClient();
  const trimmed = term.trim();
  return useQuery({
    queryKey: [KEY, 'search', trimmed],
    queryFn: () => get<ItemsResponse>(`/jellyfin/_officer/search${query({ q: trimmed })}`),
    enabled: enabled && trimmed.length > 1,
    staleTime: 30_000,
  });
}

/** Watched and favourite marks. Both invalidate everything, because both change Next Up and Resume. */
export function useItemFlags() {
  const { post, delete: del } = useClient();
  const qc = useQueryClient();
  const invalidate = () => qc.invalidateQueries({ queryKey: [KEY] });

  const setPlayed = useMutation({
    mutationFn: ({ id, played }: { id: string; played: boolean }) =>
      played ? post(`/jellyfin/_officer/items/${id}/played`, {}) : del(`/jellyfin/_officer/items/${id}/played`),
    onSuccess: invalidate,
  });

  const setFavorite = useMutation({
    mutationFn: ({ id, favorite }: { id: string; favorite: boolean }) =>
      favorite ? post(`/jellyfin/_officer/items/${id}/favorite`, {}) : del(`/jellyfin/_officer/items/${id}/favorite`),
    onSuccess: invalidate,
  });

  return { setPlayed, setFavorite };
}

// ── Playback ──────────────────────────────────────────────────────────────────────────────────────

export type PlaybackRequest = {
  id: string;
  startSeconds: number;
  mediaSourceId?: string;
  audioStreamIndex?: number;
  /** A rung the owner picked. Omitted means Auto — the server decides whether it can copy the video. */
  maxStreamingBitrate?: number;
};

/**
 * Negotiate a stream. A mutation rather than a query on purpose: it has a side effect on the server —
 * `AutoOpenLiveStream` starts an ffmpeg process for a transcode — so it must never be replayed by a refetch,
 * a window focus, or React's strict-mode double render.
 */
export function usePlaybackPlan() {
  const { post } = useClient();
  return useMutation({
    mutationFn: ({ id, startSeconds, mediaSourceId, audioStreamIndex, maxStreamingBitrate }: PlaybackRequest) =>
      post<PlaybackPlan>(`/jellyfin/_officer/items/${id}/playback${query({ startSeconds })}`, {
        mediaSourceId,
        audioStreamIndex,
        maxStreamingBitrate,
      }),
  });
}

export type PlaystateReport = {
  ItemId: string;
  PlaySessionId?: string | null;
  MediaSourceId?: string | null;
  PositionTicks: number;
  IsPaused?: boolean;
  CanSeek?: boolean;
};

/**
 * Tell Jellyfin where playback is, so resume works and the server knows a session is alive.
 *
 * `fire-and-forget` is deliberate: a dropped progress ping costs a few seconds of resume accuracy, and
 * surfacing it as an error over the video would be a worse trade. The stop report is the one that matters,
 * and it is also the one most likely to race a page unload — hence `sendBeacon`-shaped semantics (no
 * response read, no await from the caller).
 */
export function usePlaystateReporter() {
  const { post } = useClient();
  const qc = useQueryClient();

  const report = (action: 'playing' | 'progress' | 'stopped', body: PlaystateReport) => {
    void post(`/jellyfin/_officer/sessions/${action}`, body).catch(() => undefined);
    // Only a stop changes what the shelves should show. Pinging progress every ten seconds and invalidating
    // each time would refetch the whole home screen underneath a playing video.
    if (action === 'stopped') void qc.invalidateQueries({ queryKey: [KEY] });
  };

  return report;
}

// ── Servers ───────────────────────────────────────────────────────────────────────────────────────
//
// Jellyfin instances and the account each is signed in as are the owner's to set from /jellyfin/settings.
// It is a REGISTRY — any number of labelled servers, one selected — because this machine genuinely runs
// several. The access token is WRITE-ONLY across this boundary: a server carries its label, URL, account name
// and whether it is selected, and has no field that could carry the token back to the browser.

export type JellyfinHealth = {
  ok: boolean;
  /** False only when no server is stored. It is what separates "set this up" from "this used to work". */
  configured?: boolean;
  /** Label of the selected server, so a failure names which one did not answer. */
  server?: string;
  serverName?: string | null;
  version?: string | null;
  ms?: number;
  error?: string;
};

export type JellyfinServerRow = {
  id: number;
  label: string;
  url: string;
  jellyfinUsername: string | null;
  serverName: string | null;
  version: string | null;
  isActive: boolean;
  lastSeenAt: string | null;
  createdAt: string;
};

export type JellyfinServers = { configured: boolean; activeId: number | null; servers: JellyfinServerRow[] };

/** Unwrap the `{ status, message }` useClient throws, where `message` is the sidecar's JSON body. */
export function jellyfinErrorMessage(err: unknown): string {
  const raw = (err as { message?: unknown } | null)?.message;
  if (typeof raw !== 'string' || !raw) return 'Something went wrong';
  try {
    const parsed = JSON.parse(raw) as { error?: unknown };
    if (typeof parsed.error === 'string' && parsed.error) return parsed.error;
  } catch {
    /* plain text */
  }
  return raw.slice(0, 300);
}

/**
 * Health, including its failure bodies.
 *
 * `get` throws on any status >= 400, so a plain query would leave `data` undefined for exactly the two cases
 * the UI most needs to tell apart — 503 not configured and 502 configured-but-broken. Both carry a JSON body,
 * so the throw is turned back into the answer rather than an error state.
 */
export function useJellyfinHealth() {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'health'],
    queryFn: async (): Promise<JellyfinHealth> => {
      try {
        return await get<JellyfinHealth>('/jellyfin/_health');
      } catch (err) {
        const raw = (err as { message?: unknown } | null)?.message;
        if (typeof raw === 'string') {
          try {
            const body = JSON.parse(raw) as JellyfinHealth;
            if (body && body.ok === false) return body;
          } catch {
            /* not the sidecar's body */
          }
        }
        // Anything else — the platform proxy, auth, the sidecar being down — is a configured server that is
        // failing, not an unconfigured one. Never offer the setup form on a guess.
        return { ok: false, configured: true, error: jellyfinErrorMessage(err) };
      }
    },
    staleTime: 60_000,
    retry: false,
  });
}

export function useJellyfinServers() {
  const { get } = useClient();
  return useQuery({
    queryKey: [KEY, 'servers'],
    queryFn: () => get<JellyfinServers>('/jellyfin/_config'),
    staleTime: 60_000,
    retry: false,
  });
}

export type AddServerInput = { label: string; url: string; username: string; password: string };
export type EditServerInput = { id: number; label?: string; url?: string; username?: string; password?: string };

export function useJellyfinServerActions() {
  const { get, post, patch, delete: del } = useClient();
  const qc = useQueryClient();
  // Adding, editing, switching and removing all change what every other query here can even answer — a switch
  // in particular changes the answer to all of them without changing any of their inputs.
  const invalidate = () => qc.invalidateQueries({ queryKey: [KEY] });

  const add = useMutation({
    mutationFn: (input: AddServerInput) => post<{ server: JellyfinServerRow }>('/jellyfin/_config', input),
    onSuccess: invalidate,
  });

  const edit = useMutation({
    mutationFn: ({ id, ...rest }: EditServerInput) =>
      patch<{ server: JellyfinServerRow }>(`/jellyfin/_config/${id}`, rest),
    onSuccess: invalidate,
  });

  const activate = useMutation({
    mutationFn: (id: number) => post<JellyfinServers>(`/jellyfin/_config/${id}/activate`, {}),
    onSuccess: invalidate,
  });

  const remove = useMutation({
    mutationFn: (id: number) => del<JellyfinServers>(`/jellyfin/_config/${id}`),
    onSuccess: invalidate,
  });

  // GET — probes one server and reports whether it answered. It does not switch to it (that is
  // `activate`) and writes nothing. useMutation is still right: it is fired by a button, not rendered.
  const test = useMutation({
    mutationFn: (id: number) =>
      get<{ ok: boolean; version?: string | null; serverName?: string | null; ms: number }>(
        `/jellyfin/_config/${id}/test`,
      ),
    onSuccess: invalidate,
  });

  return { add, edit, activate, remove, test };
}
