import type { Torrent } from './shared';
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { STATUS } from './shared';

// Filtering and sorting for the torrent list, with every knob in the query string.
//
// The nav panel writes these params and the table panel reads them; they never talk to each other. That is
// the whole reason the state lives in the URL rather than in a channel — two sibling panels, one address,
// and a filtered view you can bookmark or send to yourself.

export const STATUS_FILTERS = [
  { id: 'all', label: 'All' },
  { id: 'active', label: 'Active' },
  { id: 'downloading', label: 'Downloading' },
  { id: 'seeding', label: 'Seeding' },
  { id: 'paused', label: 'Paused' },
  { id: 'finished', label: 'Finished' },
  { id: 'checking', label: 'Verifying' },
  { id: 'error', label: 'Error' },
] as const;

export type StatusFilterId = (typeof STATUS_FILTERS)[number]['id'];

const isStatusFilter = (v: string | null): v is StatusFilterId => !!v && STATUS_FILTERS.some((f) => f.id === v);

export type SortKey =
  | 'name'
  | 'queuePosition'
  | 'addedDate'
  | 'doneDate'
  | 'activityDate'
  | 'percentDone'
  | 'totalSize'
  | 'rateDownload'
  | 'rateUpload'
  | 'uploadRatio'
  | 'status'
  | 'eta'
  | 'downloadedEver'
  | 'uploadedEver'
  | 'peersSendingToUs'
  | 'peersGettingFromUs'
  | 'secondsSeeding'
  | 'downloadDir'
  | 'officerTrackerHost'
  | 'id';

const DEFAULT_SORT: SortKey = 'queuePosition';

/** Param names, in one place so the nav, the table header and the screen guard cannot drift. */
const P = {
  q: 'q',
  status: 'status',
  label: 'label',
  tracker: 'tracker',
  dir: 'dir',
  error: 'err',
  sort: 'sort',
  desc: 'desc',
  selected: 'selected',
} as const;

export type TorrentFilters = {
  q: string;
  status: StatusFilterId;
  label: string | null;
  tracker: string | null;
  downloadDir: string | null;
  errorOnly: boolean;
  sort: SortKey;
  desc: boolean;
  /** Any of the narrowing filters (not sort, not selection) is engaged. */
  isFiltered: boolean;
};

export function useTorrentFilters() {
  const [params, setParams] = useSearchParams();

  const rawStatus = params.get(P.status);
  const filters = useMemo<TorrentFilters>(() => {
    const q = params.get(P.q) ?? '';
    const status = isStatusFilter(rawStatus) ? rawStatus : 'all';
    const label = params.get(P.label);
    const tracker = params.get(P.tracker);
    const downloadDir = params.get(P.dir);
    const errorOnly = params.get(P.error) === '1';
    return {
      q,
      status,
      label,
      tracker,
      downloadDir,
      errorOnly,
      sort: (params.get(P.sort) as SortKey) || DEFAULT_SORT,
      desc: params.get(P.desc) === '1',
      isFiltered: !!q || status !== 'all' || !!label || !!tracker || !!downloadDir || errorOnly,
    };
    // params is a fresh URLSearchParams object on every render, so depend on the values instead of the
    // object — otherwise every render produces a new filters object and re-sorts the whole list.
  }, [
    params.get(P.q),
    rawStatus,
    params.get(P.label),
    params.get(P.tracker),
    params.get(P.dir),
    params.get(P.error),
    params.get(P.sort),
    params.get(P.desc),
  ]);

  /**
   * Writes go through the functional form of setSearchParams so concurrent updates (typing in the search
   * box while a filter menu closes) compose instead of clobbering each other. `replace` keeps the back
   * button useful: forty keystrokes should not be forty history entries.
   */
  const patch = useCallback(
    (next: Partial<Record<keyof typeof P, string | null>>, replace = true) => {
      setParams(
        (prev) => {
          const out = new URLSearchParams(prev);
          for (const [key, value] of Object.entries(next)) {
            const param = P[key as keyof typeof P];
            if (value === null || value === '') out.delete(param);
            else out.set(param, value);
          }
          return out;
        },
        { replace },
      );
    },
    [setParams],
  );

  const setSearch = useCallback((q: string) => patch({ q: q || null }), [patch]);
  const setStatus = useCallback((s: StatusFilterId) => patch({ status: s === 'all' ? null : s }), [patch]);

  /** Menu entries toggle: clicking the one already applied clears it. Saves a separate "clear" affordance. */
  const toggle = useCallback(
    (key: 'label' | 'tracker' | 'dir' | 'error', value: string, current: string | null) =>
      patch({ [key]: current === value ? null : value }),
    [patch],
  );

  const clearFilters = useCallback(
    () => patch({ q: null, status: null, label: null, tracker: null, dir: null, error: null }, false),
    [patch],
  );

  /**
   * Sorting toggles direction when you pick the column that is already sorted. New columns start ascending,
   * except the ones where "most" is the interesting end — nobody opens a torrent list to see the slowest.
   */
  const setSort = useCallback(
    (key: SortKey, currentSort: SortKey, currentDesc: boolean) => {
      if (key === currentSort) return patch({ sort: key, desc: currentDesc ? null : '1' });
      return patch({ sort: key, desc: DESC_BY_DEFAULT.has(key) ? '1' : null });
    },
    [patch],
  );

  const selectedId = Number(params.get(P.selected));
  const selected = Number.isFinite(selectedId) && selectedId > 0 ? selectedId : null;
  const setSelected = useCallback((id: number | null) => patch({ selected: id ? String(id) : null }), [patch]);

  return { filters, setSearch, setStatus, toggle, clearFilters, setSort, selected, setSelected };
}

const DESC_BY_DEFAULT = new Set<SortKey>([
  'addedDate',
  'doneDate',
  'activityDate',
  'rateDownload',
  'rateUpload',
  'uploadRatio',
  'totalSize',
  'downloadedEver',
  'uploadedEver',
  'peersSendingToUs',
  'peersGettingFromUs',
  'secondsSeeding',
  'percentDone',
]);

// ── Predicates ────────────────────────────────────────────────────────────────────────────────────

function matchesStatus(t: Torrent, filter: StatusFilterId): boolean {
  switch (filter) {
    case 'all':
      return true;
    // "Active" means moving bytes right now, which is not the same as "not stopped" — a seeding torrent
    // with no peers is running but idle, and lumping it in here makes the filter useless.
    case 'active':
      return t.rateDownload > 0 || t.rateUpload > 0;
    case 'downloading':
      return t.status === STATUS.Download || t.status === STATUS.DownloadWait;
    case 'seeding':
      return t.status === STATUS.Seed || t.status === STATUS.SeedWait;
    case 'paused':
      return t.status === STATUS.Stopped;
    case 'finished':
      return t.percentDone >= 1;
    case 'checking':
      return t.status === STATUS.Check || t.status === STATUS.CheckWait;
    case 'error':
      return t.error !== 0;
  }
}

const compare = (a: Torrent, b: Torrent, key: SortKey): number => {
  const av = a[key as keyof Torrent];
  const bv = b[key as keyof Torrent];
  if (typeof av === 'string' || typeof bv === 'string') {
    return String(av ?? '').localeCompare(String(bv ?? ''), undefined, { numeric: true, sensitivity: 'base' });
  }
  return Number(av ?? 0) - Number(bv ?? 0);
};

/**
 * Applies the filters and the sort. Pure, and memoised by the caller — 43 torrents is nothing but the list
 * re-renders every 5 seconds forever, and this also runs on every keystroke in the search box.
 */
export function selectTorrents(torrents: Torrent[], filters: TorrentFilters): Torrent[] {
  const needle = filters.q.trim().toLowerCase();

  const out = torrents.filter((t) => {
    if (!matchesStatus(t, filters.status)) return false;
    if (filters.errorOnly && t.error === 0) return false;
    if (filters.label && !t.labels.includes(filters.label)) return false;
    if (filters.tracker && t.officerTrackerHost !== filters.tracker) return false;
    if (filters.downloadDir && t.downloadDir !== filters.downloadDir) return false;
    if (needle && !t.name.toLowerCase().includes(needle) && !t.labels.some((l) => l.toLowerCase().includes(needle)))
      return false;
    return true;
  });

  out.sort((a, b) => {
    const primary = compare(a, b, filters.sort);
    // Ties on a coarse key (status, priority, a rate of zero) would otherwise reshuffle on every poll,
    // because Array.sort is only stable with respect to the input order and the input is a fresh fetch.
    return (filters.desc ? -primary : primary) || a.id - b.id;
  });

  return out;
}

/** The distinct values the nav offers as filters, each with how many torrents carry it. */
export type FacetEntry = { value: string; count: number };

export function buildFacets(torrents: Torrent[]) {
  const labels = new Map<string, number>();
  const trackers = new Map<string, number>();
  const dirs = new Map<string, number>();
  let errors = 0;

  for (const t of torrents) {
    for (const label of t.labels) labels.set(label, (labels.get(label) ?? 0) + 1);
    if (t.officerTrackerHost) trackers.set(t.officerTrackerHost, (trackers.get(t.officerTrackerHost) ?? 0) + 1);
    if (t.downloadDir) dirs.set(t.downloadDir, (dirs.get(t.downloadDir) ?? 0) + 1);
    if (t.error !== 0) errors += 1;
  }

  const toSorted = (m: Map<string, number>): FacetEntry[] =>
    [...m.entries()]
      .map(([value, count]) => ({ value, count }))
      .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));

  return { labels: toSorted(labels), trackers: toSorted(trackers), dirs: toSorted(dirs), errors };
}

/** Per-status counts for the nav badges, computed in one pass rather than eight filters. */
export function countByStatus(torrents: Torrent[]): Record<StatusFilterId, number> {
  const counts = Object.fromEntries(STATUS_FILTERS.map((f) => [f.id, 0])) as Record<StatusFilterId, number>;
  for (const t of torrents) {
    for (const f of STATUS_FILTERS) if (matchesStatus(t, f.id)) counts[f.id] += 1;
  }
  return counts;
}
