import { useState, useEffect, useCallback } from 'react';
import { Link, useSearchParams } from 'react-router';
import { useClient } from 'hooks/useClient';
import { toast } from 'sonner';
import { Search, Loader2, RefreshCw, X, History, FileAudio, Users, ChevronRight, Trash2 } from 'lucide-react';
import { SearchResults, dropCachedResults } from './SearchResults';
import { formatWhen, SEARCH_PARAM, type SlskdSearchSummary } from './shared';
import { randomId } from 'helpers/random-id';

// The 'search' section — a search input plus the history of past searches (GET /searches). Which search
// is open lives in `?search=<id>`, so rows are real links and the back button returns to the history.
//
// Submitting opens the new search's results straight away, the way slskd's own web UI does. That is not
// only a nicety: a history row is only as fresh as the last list load, so a search you had just started
// sat at 0 responses looking stalled, while the results view polls for as long as the search runs.

const HISTORY_POLL_MS = 4000;
// A running search nobody is watching shouldn't poll forever (~3 minutes).
const MAX_HISTORY_POLLS = 45;

const byNewest = (list: SlskdSearchSummary[]) => [...list].sort((a, b) => b.startedAt.localeCompare(a.startedAt));

// slskd accepts a client-supplied id (its own UI depends on that), but don't bet the navigation on it:
// prefer an id the server has actually stored, then fall back to the newest search with this text.
type CreatedIdParams = { list: SlskdSearchSummary[] | null; sentId: string; returnedId?: string; text: string };
const createdSearchId = ({ list, sentId, returnedId, text }: CreatedIdParams) => {
  if (!list) return returnedId ?? sentId;
  const known = new Set(list.map((s) => s.id));
  if (returnedId && known.has(returnedId)) return returnedId;
  if (known.has(sentId)) return sentId;
  return list.find((s) => s.searchText === text)?.id ?? returnedId ?? sentId;
};

export const SearchView = () => {
  const client = useClient();
  const [params, setParams] = useSearchParams();
  const [query, setQuery] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [history, setHistory] = useState<SlskdSearchSummary[] | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [clearing, setClearing] = useState(false);
  const [confirmClear, setConfirmClear] = useState(false);

  const selectedId = params.get(SEARCH_PARAM);

  const openSearch = useCallback(
    (id: string | null) =>
      setParams((prev) => {
        const next = new URLSearchParams(prev);
        if (id) next.set(SEARCH_PARAM, id);
        else next.delete(SEARCH_PARAM);
        return next;
      }),
    [setParams],
  );

  const fetchHistory = useCallback(async () => {
    const list = await client.get<SlskdSearchSummary[]>('/slskd/api/v0/searches');
    return byNewest(list ?? []);
    // useClient() is a fresh object each render — keep this callback stable.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const load = useCallback(() => {
    setError(null);
    fetchHistory()
      .then(setHistory)
      .catch((err) => setError(err instanceof Error ? err.message : String(err)));
  }, [fetchHistory]);

  // Re-read whenever the history is what's on screen — including a browser Back out of a search, which
  // no callback of ours sees.
  useEffect(() => {
    if (!selectedId) load();
  }, [selectedId, load]);

  // A row's counters only move when the list is re-read, so keep re-reading while a search is running —
  // but not while its results are open, since that view polls on its own.
  const running = !!history?.some((s) => !s.isComplete);
  useEffect(() => {
    if (!running || selectedId) return;
    let polls = 0;
    const timer = setInterval(() => {
      polls += 1;
      if (polls > MAX_HISTORY_POLLS) {
        clearInterval(timer);
        return;
      }
      load();
    }, HISTORY_POLL_MS);
    return () => clearInterval(timer);
  }, [running, selectedId, load]);

  const submit = async (ev: React.FormEvent) => {
    ev.preventDefault();
    const text = query.trim();
    if (!text || submitting) return;
    setSubmitting(true);
    const sentId = randomId();
    try {
      const created = await client.post<Partial<SlskdSearchSummary>>('/slskd/api/v0/searches', {
        id: sentId,
        searchText: text,
      });
      // A failed reload shouldn't read as a failed search — fall through with what the POST told us.
      const list = await fetchHistory().catch(() => null);
      if (list) setHistory(list);
      setQuery('');
      openSearch(createdSearchId({ list, sentId, returnedId: created?.id, text }));
    } catch (err) {
      toast.error(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
    } finally {
      setSubmitting(false);
    }
  };

  const remove = async (id: string) => {
    setHistory((prev) => prev?.filter((s) => s.id !== id) ?? prev);
    dropCachedResults(id);
    if (selectedId === id) openSearch(null);
    client.delete(`/slskd/api/v0/searches/${id}`).catch(() => load());
  };

  // slskd has no bulk delete (0.26.0 exposes only DELETE /searches/{id}), so clearing fans out one
  // request per search and reconciles with a reload — partial failures come back on their own.
  const clearAll = async () => {
    const ids = history?.map((s) => s.id) ?? [];
    if (!ids.length || clearing) return;
    setConfirmClear(false);
    setClearing(true);
    setHistory([]);
    openSearch(null);
    ids.forEach(dropCachedResults);
    const settled = await Promise.allSettled(ids.map((id) => client.delete(`/slskd/api/v0/searches/${id}`)));
    const failed = settled.filter((r) => r.status === 'rejected').length;
    if (failed) toast.error(`${failed} of ${ids.length} searches could not be removed.`);
    setClearing(false);
    load();
  };

  // The destructive click is two-step rather than a modal (matching the other Soulseek panels, which
  // confirm nothing) — and the armed state disarms itself so it can't sit there waiting to be hit.
  useEffect(() => {
    if (!confirmClear) return;
    const timer = setTimeout(() => setConfirmClear(false), 4000);
    return () => clearTimeout(timer);
  }, [confirmClear]);

  // The row we came from, when we have it — a search opened by URL (or just created) resolves its own.
  const selected = selectedId ? (history?.find((s) => s.id === selectedId) ?? null) : null;
  const hrefFor = (id: string) => {
    const next = new URLSearchParams(params);
    next.set(SEARCH_PARAM, id);
    return `?${next.toString()}`;
  };

  if (selectedId) return <SearchResults searchId={selectedId} initial={selected} onBack={() => openSearch(null)} />;

  return (
    <div className="flex h-full flex-col overflow-hidden">
      {/* Header + search field */}
      <div className="shrink-0 border-b bg-background/60 px-6 py-5">
        <h1 className="mb-3 text-lg font-semibold">Search</h1>
        <form onSubmit={submit} className="flex gap-2">
          <div className="relative flex-1">
            <Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-500" />
            <input
              value={query}
              onChange={(ev) => setQuery(ev.target.value)}
              placeholder="Search artists, albums, tracks…"
              className="h-11 w-full rounded-xl border border-white/10 bg-zinc-950 pl-10 pr-3 text-sm text-zinc-100 placeholder:text-zinc-500 shadow-sm outline-none transition focus:border-primary/50 focus:ring-2 focus:ring-primary/20"
            />
          </div>
          <button
            type="submit"
            disabled={submitting || !query.trim()}
            className="flex h-11 items-center gap-2 rounded-xl bg-primary px-5 text-sm font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 disabled:opacity-50"
          >
            {submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Search className="h-4 w-4" />}
            Search
          </button>
        </form>
      </div>

      {/* History */}
      <div className="flex shrink-0 items-center justify-between px-6 pb-2 pt-4">
        <div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-white">
          <History className="h-3.5 w-3.5" />
          Recent searches
          {history && <span>· {history.length}</span>}
        </div>
        <div className="flex items-center gap-1">
          {!!history?.length && (
            <button
              type="button"
              onClick={() => (confirmClear ? clearAll() : setConfirmClear(true))}
              onBlur={() => setConfirmClear(false)}
              disabled={clearing}
              title={confirmClear ? `Delete all ${history.length} searches` : 'Clear all searches'}
              className={`flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-white transition disabled:opacity-50 ${
                confirmClear ? 'bg-red-500/20 hover:bg-red-500/30' : 'hover:bg-white/10'
              }`}
            >
              {clearing ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Trash2 className="h-3.5 w-3.5" />}
              {clearing ? 'Clearing…' : confirmClear ? `Delete ${history.length}?` : 'Clear all'}
            </button>
          )}
          <button
            type="button"
            onClick={load}
            title="Refresh"
            className="flex h-7 w-7 items-center justify-center rounded-md text-white transition hover:bg-white/10"
          >
            <RefreshCw className="h-3.5 w-3.5" />
          </button>
        </div>
      </div>

      <div className="min-h-0 flex-1 overflow-y-auto px-4 pb-4">
        {error && <p className="px-2 py-3 text-sm text-red-500">Could not load history: {error}</p>}
        {!error && history === null && (
          <div className="flex items-center gap-2 px-2 py-6 text-sm text-zinc-500">
            <Loader2 className="h-4 w-4 animate-spin" /> Loading…
          </div>
        )}
        {!error && history?.length === 0 && (
          <div className="flex flex-col items-center gap-2 px-2 py-16 text-center text-sm text-zinc-500">
            <History className="h-8 w-8 opacity-40" />
            No searches yet — try one above.
          </div>
        )}
        {history && history.length > 0 && (
          <ul className="flex flex-col gap-2">
            {history.map((s) => (
              <li
                key={s.id}
                className="flex items-center gap-3 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2.5 shadow-sm transition-colors hover:bg-white/5"
              >
                {/* The row is an anchor so it opens with cmd-click and keyboard like any link; the remove
                    button is its sibling, never nested inside it. */}
                <Link to={hrefFor(s.id)} className="flex min-w-0 flex-1 items-center gap-3">
                  <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-white/5 text-zinc-400">
                    {s.isComplete ? <Search className="h-4 w-4" /> : <Loader2 className="h-4 w-4 animate-spin" />}
                  </div>
                  <div className="min-w-0 flex-1">
                    <div className="truncate text-sm font-medium text-zinc-100">{s.searchText}</div>
                    <div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-zinc-500">
                      <span>{formatWhen(s.startedAt)}</span>
                      <span className="inline-flex items-center gap-1">
                        <Users className="h-3 w-3" />
                        {s.responseCount.toLocaleString()}
                      </span>
                      <span className="inline-flex items-center gap-1">
                        <FileAudio className="h-3 w-3" />
                        {s.fileCount.toLocaleString()}
                      </span>
                    </div>
                  </div>
                </Link>
                <button
                  type="button"
                  onClick={() => remove(s.id)}
                  title="Remove search"
                  className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-zinc-500 transition hover:bg-red-500/10 hover:text-red-400"
                >
                  <X className="h-4 w-4" />
                </button>
                <ChevronRight className="h-4 w-4 shrink-0 text-zinc-600" />
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
};
