import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';
import type { SoulseekBrowseSnapshot, SoulseekBrowseLevel, SoulseekBrowseSearch, SoulseekBrowsedFile } from './shared';

// Client for the sidecar's cached share trees. The expensive work happens server-side and outlives the
// tab, so this hook only ever starts a fetch and polls for its status — it never pulls a whole tree.

const SNAPSHOTS_KEY = ['soulseek', 'browse'] as const;
const EMPTY: SoulseekBrowseSnapshot[] = [];

// While something is fetching we need to notice it finishing; otherwise this data barely changes.
const POLL_MS = 3000;

/**
 * Every peer's cache state in one request, so the favourites list can badge each row without N queries.
 * Polls only while at least one fetch is in flight.
 */
export function useSoulseekBrowseSnapshots() {
  const { get, post, delete: del } = useClient();
  const qc = useQueryClient();

  const { data } = useQuery({
    queryKey: SNAPSHOTS_KEY,
    queryFn: () => get<SoulseekBrowseSnapshot[]>('/slskd/_officer/browse'),
    staleTime: 10_000,
    refetchInterval: (query) => (query.state.data?.some((s) => s.status === 'pending') ? POLL_MS : false),
  });

  const snapshots = data ?? EMPTY;
  const snapshotOf = (username: string) => snapshots.find((s) => s.username === username) ?? null;

  const start = useMutation({
    mutationFn: (username: string) => post(`/slskd/_officer/browse/${encodeURIComponent(username)}`, {}),
    // Flip to pending immediately so the row shows a spinner before the first poll lands.
    onMutate: (username) => {
      const prev = qc.getQueryData<SoulseekBrowseSnapshot[]>(SNAPSHOTS_KEY) ?? EMPTY;
      const existing = prev.find((s) => s.username === username);
      const pending: SoulseekBrowseSnapshot = {
        ...(existing ?? {
          username,
          directoryCount: 0,
          fileCount: 0,
          totalSize: 0,
          completedAt: null,
        }),
        status: 'pending',
        error: null,
        startedAt: new Date().toISOString(),
      };
      qc.setQueryData<SoulseekBrowseSnapshot[]>(
        SNAPSHOTS_KEY,
        existing ? prev.map((s) => (s.username === username ? pending : s)) : [...prev, pending],
      );
      return { prev };
    },
    onError: (_err, _username, ctx) => {
      if (ctx?.prev) qc.setQueryData(SNAPSHOTS_KEY, ctx.prev);
    },
    onSettled: () => qc.invalidateQueries({ queryKey: SNAPSHOTS_KEY }),
  });

  const drop = useMutation({
    mutationFn: (username: string) => del(`/slskd/_officer/browse/${encodeURIComponent(username)}`),
    onSettled: () => qc.invalidateQueries({ queryKey: SNAPSHOTS_KEY }),
  });

  return {
    snapshots,
    snapshotOf,
    fetchShares: (username: string) => start.mutate(username),
    dropShares: (username: string) => drop.mutate(username),
  };
}

type LevelParams = { username: string | null; parent: string | null; limit: number; enabled?: boolean };

/**
 * One level of a peer's tree — the children of `parent`, or the roots when it's null. Cached per level,
 * so collapsing and reopening a folder is free, and expanding a shallow node never touches its subtree.
 */
export function useSoulseekBrowseLevel({ username, parent, limit, enabled = true }: LevelParams) {
  const { get } = useClient();

  return useQuery({
    queryKey: ['soulseek', 'browse', username, 'level', parent, limit],
    queryFn: () => {
      const params = new URLSearchParams({ limit: String(limit) });
      if (parent) params.set('parent', parent);
      return get<SoulseekBrowseLevel>(`/slskd/_officer/browse/${encodeURIComponent(username ?? '')}/tree?${params}`);
    },
    enabled: enabled && !!username,
    staleTime: 5 * 60_000,
  });
}

type SearchParams = { username: string | null; q: string; enabled?: boolean };

/** The filtered tree: matches with their ancestors, in one request, ready to render pre-expanded. */
export function useSoulseekBrowseSearch({ username, q, enabled = true }: SearchParams) {
  const { get } = useClient();

  return useQuery({
    queryKey: ['soulseek', 'browse', username, 'search', q],
    queryFn: () =>
      get<SoulseekBrowseSearch>(
        `/slskd/_officer/browse/${encodeURIComponent(username ?? '')}/tree/search?q=${encodeURIComponent(q)}`,
      ),
    enabled: enabled && !!username && !!q,
    // The previous result stays on screen while the next one loads, so the tree doesn't blink per keystroke.
    placeholderData: (prev) => prev,
    staleTime: 60_000,
  });
}

/** A folder (its whole subtree) or one file inside it, named the way the cache stores them. */
export type SoulseekDownloadTarget = { path: string; file?: string };

/**
 * Queue a download from the cache. The sidecar expands a folder into its files and rejoins each one to
 * its peer path, so this sends a path rather than a list — the browser never walks the subtree itself.
 */
export function useSoulseekEnqueue(username: string) {
  const { post } = useClient();

  return useMutation({
    mutationFn: (target: SoulseekDownloadTarget) =>
      post<{ queued: number }>(`/slskd/_officer/browse/${encodeURIComponent(username)}/download`, target),
    onSuccess: ({ queued }, target) =>
      toast.success(queued === 1 ? `Queued ${target.file ?? basename(target.path)}` : `Queued ${queued} files`),
    onError: (err) => toast.error(`Download failed: ${err instanceof Error ? err.message : String(err)}`),
  });
}

const basename = (path: string) => path.slice(path.lastIndexOf('\\') + 1);

/** A single folder's files, fetched only when it's expanded. Cached indefinitely — the rows are immutable. */
export function useSoulseekBrowseFiles(username: string | null, dirId: number | null) {
  const { get } = useClient();

  return useQuery({
    queryKey: ['soulseek', 'browse', username, 'dir', dirId],
    queryFn: () =>
      get<SoulseekBrowsedFile[]>(`/slskd/_officer/browse/${encodeURIComponent(username ?? '')}/dirs/${dirId}/files`),
    enabled: !!username && dirId !== null,
    staleTime: Infinity,
  });
}
