import type { SessionSettings, SessionStats, Torrent, TorrentAction } from './shared';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';

// Data layer for the /transmission panels. Everything talks to the officer-transmission sidecar through the
// /api/transmission auth proxy.
//
// Transmission has no push channel — it is poll-only RPC — so the list refetches on an interval rather than
// subscribing. Mutations invalidate the whole ['transmission'] prefix instead of patching caches: starting
// one torrent changes the session speed totals and the queue positions of every torrent below it, so a
// targeted patch would be wrong more often than it was cheap.

const TORRENTS_KEY = ['transmission', 'torrents'] as const;
const SESSION_KEY = ['transmission', 'session'] as const;
const STATS_KEY = ['transmission', 'stats'] as const;

const EMPTY_TORRENTS: Torrent[] = [];

/** How often the list re-reads. Matches the reference client's default and is cheap against loopback. */
const LIST_POLL_MS = 5_000;
/** Session settings change only when someone edits them, so this is a safety net, not a live feed. */
const SESSION_POLL_MS = 60_000;

export function useTorrents() {
  const { get } = useClient();

  const query = useQuery({
    queryKey: TORRENTS_KEY,
    queryFn: () => get<{ torrents: Torrent[] }>('/transmission/_officer/torrents'),
    refetchInterval: LIST_POLL_MS,
    // Anything shorter than the poll would make an unrelated remount trigger a second fetch immediately.
    staleTime: LIST_POLL_MS - 1_000,
  });

  return {
    torrents: query.data?.torrents ?? EMPTY_TORRENTS,
    isLoading: query.isLoading,
    error: query.error,
  };
}

/**
 * One torrent with the expensive arrays (files, peers, trackers). Kept separate from the list because those
 * arrays are per-file and per-peer — pulling them for every row on every poll is what makes naive
 * Transmission clients hammer the daemon.
 */
export function useTorrentDetail(id: number | null) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: ['transmission', 'torrent', id] as const,
    queryFn: () => get<{ torrent: Torrent }>(`/transmission/_officer/torrents/${id}`),
    enabled: id != null,
    refetchInterval: LIST_POLL_MS,
    staleTime: LIST_POLL_MS - 1_000,
  });

  return { torrent: query.data?.torrent ?? null, isLoading: query.isLoading, error: query.error };
}

export function useSessionStats() {
  const { get } = useClient();

  const query = useQuery({
    queryKey: STATS_KEY,
    queryFn: () => get<{ stats: SessionStats }>('/transmission/_officer/stats'),
    refetchInterval: LIST_POLL_MS,
    staleTime: LIST_POLL_MS - 1_000,
  });

  return { stats: query.data?.stats ?? null, isLoading: query.isLoading, error: query.error };
}

export function useSession() {
  const { get, post } = useClient();
  const qc = useQueryClient();

  const query = useQuery({
    queryKey: SESSION_KEY,
    queryFn: () => get<{ session: SessionSettings }>('/transmission/_officer/session'),
    refetchInterval: SESSION_POLL_MS,
    staleTime: 30_000,
  });

  const save = useMutation({
    mutationFn: (patch: Record<string, unknown>) =>
      post<{ session: SessionSettings }>('/transmission/_officer/session', patch),
    onSuccess: (data) => {
      // The sidecar reads the settings back after writing, because Transmission clamps several of them.
      // Seeding the cache with that response means the form shows what the daemon actually holds.
      qc.setQueryData(SESSION_KEY, data);
      toast.success('Settings saved');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not save settings')),
  });

  return {
    session: query.data?.session ?? null,
    isLoading: query.isLoading,
    error: query.error,
    save,
  };
}

/**
 * Free space at a path, as the daemon sees it. Worth stating plainly: this is the *daemon's* filesystem,
 * which for a containerised Transmission is not the same namespace as the platform's — the number is
 * meaningful, the path may not resolve to anything on this host.
 */
export function useFreeSpace(path: string | undefined) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: ['transmission', 'free-space', path] as const,
    queryFn: () =>
      get<{ path: string; bytes: number }>(`/transmission/_officer/free-space?path=${encodeURIComponent(path!)}`),
    enabled: !!path,
    staleTime: 60_000,
  });

  return { bytes: query.data?.bytes ?? null, isLoading: query.isLoading, error: query.error };
}

export type AddTorrentInput = {
  metainfo?: string;
  filename?: string;
  downloadDir?: string;
  labels?: string[];
  paused?: boolean;
  bandwidthPriority?: number;
  sequentialDownload?: boolean;
};

export type AddTorrentResult = { status: 'added' | 'duplicate'; torrent: { id: number; name: string } | null };

export function useTorrentMutations() {
  const { post } = useClient();
  const qc = useQueryClient();
  const invalidate = () => qc.invalidateQueries({ queryKey: ['transmission'] });

  const act = useMutation({
    mutationFn: ({ ids, action }: { ids: number[]; action: TorrentAction }) =>
      post<{ ok: true; affected: number }>('/transmission/_officer/torrents/action', { ids, action }),
    onSuccess: invalidate,
    onError: (err) => toast.error(errorMessage(err, 'Action failed')),
  });

  const set = useMutation({
    mutationFn: ({ ids, ...fields }: { ids: number[] } & Record<string, unknown>) =>
      post<{ ok: true }>('/transmission/_officer/torrents/set', { ids, ...fields }),
    onSuccess: invalidate,
    onError: (err) => toast.error(errorMessage(err, 'Could not apply the change')),
  });

  const remove = useMutation({
    mutationFn: ({ ids, deleteLocalData }: { ids: number[]; deleteLocalData: boolean }) =>
      post<{ ok: true; affected: number }>('/transmission/_officer/torrents/remove', { ids, deleteLocalData }),
    onSuccess: (data, vars) => {
      invalidate();
      toast.success(
        `Removed ${vars.ids.length} torrent${vars.ids.length === 1 ? '' : 's'}${
          vars.deleteLocalData ? ' and their data' : ''
        }`,
      );
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not remove')),
  });

  const setLocation = useMutation({
    mutationFn: ({ ids, location, move }: { ids: number[]; location: string; move: boolean }) =>
      post<{ ok: true }>('/transmission/_officer/torrents/location', { ids, location, move }),
    onSuccess: () => {
      invalidate();
      toast.success('Location updated');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not change location')),
  });

  const rename = useMutation({
    mutationFn: ({ id, path, name }: { id: number; path: string; name: string }) =>
      post<{ ok: true }>('/transmission/_officer/torrents/rename', { id, path, name }),
    onSuccess: () => {
      invalidate();
      toast.success('Renamed');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not rename')),
  });

  const add = useMutation({
    mutationFn: (input: AddTorrentInput) => post<AddTorrentResult>('/transmission/_officer/torrents/add', input),
    onSuccess: invalidate,
    onError: (err) => toast.error(errorMessage(err, 'Could not add torrent')),
  });

  return { act, set, remove, setLocation, rename, add };
}

export function useMaintenance() {
  const { get, post } = useClient();
  const qc = useQueryClient();

  const portTest = useMutation({
    // GET — it asks the daemon a question and changes nothing on either side. Still a useMutation
    // because it is fired by a button rather than rendered from cache: that is a UI concern, not an
    // HTTP one. blocklistUpdate below stays POST, because it really does refetch and replace the list.
    mutationFn: () => get<{ open: boolean }>('/transmission/_officer/port-test'),
    onSuccess: (data) => (data.open ? toast.success('Peer port is open') : toast.error('Peer port is closed')),
    onError: (err) => toast.error(errorMessage(err, 'Port test failed')),
  });

  const updateBlocklist = useMutation({
    mutationFn: () => post<{ size: number }>('/transmission/_officer/blocklist-update'),
    onSuccess: (data) => {
      qc.invalidateQueries({ queryKey: SESSION_KEY });
      toast.success(`Blocklist updated — ${data.size.toLocaleString()} rules`);
    },
    onError: (err) => toast.error(errorMessage(err, 'Blocklist update failed')),
  });

  return { portTest, updateBlocklist };
}

/**
 * useClient rejects with `{status, message}` rather than an Error, and the sidecar's message is a JSON body.
 * Unwrap both so the toast says "transmission upstream unreachable" instead of "[object Object]".
 */
function errorMessage(err: unknown, fallback: string): string {
  const raw = typeof err === 'object' && err !== null && 'message' in err ? String(err.message) : '';
  if (!raw) return fallback;
  try {
    const parsed = JSON.parse(raw) as { error?: string };
    if (parsed.error) return parsed.error;
  } catch {
    /* not JSON — use it as-is */
  }
  return raw || fallback;
}
