import { useCallback, useEffect, useRef, useState } from 'react';
import { CheckSquare, FolderPlus, ListVideo, Save, Search, TriangleAlert, X } from 'lucide-react';
import { Button, Card, Input, Spinner, Toast } from '../components';
import {
  getAllSearchItemIds,
  getSearchResults,
  messageFromSearchError,
  toAddItemsRequestFromSearch,
  type LibraryBrowseItem,
  type SearchResults
} from '../api';
import { navigateToPath } from '../routing';
import { MediaPosterCard } from '../media/MediaPosterCard';
import { mediaDetailPath } from '../media/mediaKinds';
import {
  AddToCollectionDialog,
  AddToMenu,
  AddToPlaylistDialog,
  SaveAsSmartCollectionDialog,
  type AddToItems
} from '../media/addTo';

const PAGE_SIZE = 50;

interface GroupDef {
  key: keyof SearchResults;
  label: string;
  // MediaBrowse slug for "See all"; null for kinds without a top-level browse page.
  slug: string | null;
}

// Order + labels mirror the legacy Blazor search page.
const GROUPS: GroupDef[] = [
  { key: 'movies', label: 'Movies', slug: 'movies' },
  { key: 'shows', label: 'TV Shows', slug: 'shows' },
  { key: 'seasons', label: 'Seasons', slug: 'seasons' },
  { key: 'episodes', label: 'Episodes', slug: 'episodes' },
  { key: 'artists', label: 'Artists', slug: 'artists' },
  { key: 'musicVideos', label: 'Music Videos', slug: 'music-videos' },
  { key: 'songs', label: 'Songs', slug: 'songs' },
  { key: 'otherVideos', label: 'Other Videos', slug: 'other-videos' },
  { key: 'images', label: 'Images', slug: 'images' },
  { key: 'remoteStreams', label: 'Remote Streams', slug: 'remote-streams' }
];

type SearchState =
  // `query` records which query produced this result set. When it no longer matches the current
  // committed query, the visible cards are stale (a refetch is in flight) — see `refreshing` below.
  | { results: SearchResults; error: null; status: 'success'; query: string }
  | { results: null; error: string; status: 'error' }
  | { results: null; error: null; status: 'loading' };

// Which whole-query / selection dialog is open. Per-card adds are owned by each card's AddToMenu.
type ActiveDialog =
  | { kind: 'collection'; items: AddToItems }
  | { kind: 'playlist'; items: AddToItems }
  | { kind: 'save-smart' }
  | null;

type Notice = { tone: 'ok' | 'error'; message: string };

function itemKey(item: LibraryBrowseItem): string {
  return `${item.mediaType}-${item.id}`;
}

export function SearchScreen() {
  const initialQuery = new URLSearchParams(window.location.search).get('query') ?? '';
  const [queryInput, setQueryInput] = useState(initialQuery);
  const [query, setQuery] = useState(initialQuery);
  const [state, setState] = useState<SearchState>({ results: null, error: null, status: 'loading' });
  const [selectMode, setSelectMode] = useState(false);
  const [selected, setSelected] = useState<Map<string, LibraryBrowseItem>>(new Map());
  const [dialog, setDialog] = useState<ActiveDialog>(null);
  const [pendingAll, setPendingAll] = useState<'collection' | 'playlist' | null>(null);
  const [notice, setNotice] = useState<Notice | null>(null);
  const activeRef = useRef(true);
  const seqRef = useRef(0);
  const lastQueryRef = useRef(initialQuery.trim());
  const hasQuery = query.trim().length > 0;

  useEffect(() => {
    activeRef.current = true;
    return () => {
      activeRef.current = false;
    };
  }, []);

  // The no-change guard matters: this timer also fires once on mount and must not wipe an
  // in-progress selection when the query is unchanged.
  useEffect(() => {
    const handle = window.setTimeout(() => {
      const trimmed = queryInput.trim();
      if (lastQueryRef.current === trimmed) {
        return;
      }
      lastQueryRef.current = trimmed;
      setQuery(trimmed);
      // A new query means a new result set — drop any stale selection (mirrors MediaBrowseScreen).
      setSelected(new Map());
      // Keep the URL shareable without remounting.
      const next = trimmed ? `/app/search?query=${encodeURIComponent(trimmed)}` : '/app/search';
      window.history.replaceState(null, '', next);
    }, 300);

    return () => window.clearTimeout(handle);
  }, [queryInput]);

  // Fetch only; state updates happen in the async callbacks (never synchronously in the effect
  // body) so results stay visible during a refetch. The empty-query "idle" case is derived from
  // `hasQuery` in render rather than stored, so this never fires for a blank query.
  const load = useCallback(() => {
    const trimmed = query.trim();
    if (!trimmed) {
      return;
    }

    const id = ++seqRef.current;
    getSearchResults({ query: trimmed, pageSize: PAGE_SIZE })
      .then((results) => {
        if (activeRef.current && id === seqRef.current) {
          setState({ results, error: null, status: 'success', query: trimmed });
        }
      })
      .catch((error: unknown) => {
        if (activeRef.current && id === seqRef.current) {
          setState({ results: null, error: messageFromSearchError(error), status: 'error' });
        }
      });
  }, [query]);

  useEffect(() => {
    load();
  }, [load]);

  const seeAll = (slug: string) => {
    navigateToPath(`/app/media?kind=${slug}&q=${encodeURIComponent(query.trim())}`);
  };

  const clearSelection = () => setSelected(new Map());

  // Screen-level Select toggle (deliberate UX deviation from Blazor's always-on corner-select):
  // OFF => cards drill in on click; ON => cards toggle selection. Turning it off clears the set.
  const toggleSelectMode = () => {
    if (selectMode) {
      clearSelection();
    }
    setSelectMode((current) => !current);
  };

  const toggleSelect = (item: LibraryBrowseItem) => {
    const key = itemKey(item);
    setSelected((current) => {
      const next = new Map(current);
      if (next.has(key)) {
        next.delete(key);
      } else {
        next.set(key, item);
      }
      return next;
    });
  };

  // Add the whole query's results (not just the loaded page): resolve every matching id, then open
  // the target dialog with a pre-built request override. Blazor ref: Search.razor AddAllTo*.
  const addAll = (kind: 'collection' | 'playlist') => {
    const trimmed = query.trim();
    if (!trimmed) {
      return;
    }

    setPendingAll(kind);
    getAllSearchItemIds(trimmed)
      .then((result) => {
        if (!activeRef.current) {
          return;
        }
        // Bind the all-items result to the query that requested it. `lastQueryRef` always holds the
        // current committed query (see the debounce effect); if it moved on, this bulk-add would be
        // scoped to the PREVIOUS query's entire result set — discard it (issue #221).
        if (lastQueryRef.current !== trimmed) {
          setPendingAll(null);
          return;
        }
        setPendingAll(null);
        setDialog({ kind, items: { requestOverride: toAddItemsRequestFromSearch(result) } });
      })
      .catch((error: unknown) => {
        if (!activeRef.current) {
          return;
        }
        setPendingAll(null);
        setNotice({ tone: 'error', message: messageFromSearchError(error, 'Unable to load all results') });
      });
  };

  const addSelection = (kind: 'collection' | 'playlist') => {
    setDialog({ kind, items: [...selected.values()] });
  };

  const onAddedToSelectionTarget = (label: string) => (name: string) => {
    setDialog(null);
    clearSelection();
    setNotice({ tone: 'ok', message: `Added to ${label} “${name}”` });
  };

  const totalMatches =
    state.status === 'success' && state.results
      ? GROUPS.reduce((sum, group) => sum + (state.results?.[group.key].totalCount ?? 0), 0)
      : 0;

  const hasResults = hasQuery && state.status === 'success' && totalMatches > 0;
  const selectionCount = selected.size;

  // A refetch is in flight when the currently-rendered success result set was produced by a query
  // other than the current committed one (results stay visible during refetch — see `load`). While
  // refreshing we keep cards visible but gate every mutation surface and show a refreshing cue, so
  // no add/select action is scoped to the stale, about-to-be-replaced result set (issue #221).
  // Gated on `hasQuery`: `load()` early-returns for a blank query, so `state` keeps the LAST
  // non-empty query's success variant forever once the query is cleared — without this gate,
  // `refreshing` would get stuck true over the "Type a query to search…" empty card (#222 review).
  const refreshing = hasQuery && state.status === 'success' && state.query !== query.trim();
  // Enter select mode only for the live result set.
  const canSelect = selectMode && !refreshing;

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <Input
          leadingIcon={<Search aria-hidden="true" size={14} />}
          onChange={(event) => setQueryInput(event.target.value)}
          placeholder="Search movies, shows, episodes, music…"
          value={queryInput}
        />
        <span className="ctv-channels-spacer" />
        {hasResults && (
          <>
            <Button
              // Entering select mode while refreshing is blocked (it would target a stale result
              // set), but exiting is always allowed — it only clears selection, not a mutation.
              disabled={refreshing && !selectMode}
              onClick={toggleSelectMode}
              size="sm"
              startIcon={<CheckSquare aria-hidden="true" size={14} />}
              variant={selectMode ? 'primary' : 'secondary'}
            >
              Select
            </Button>
            <Button
              disabled={pendingAll !== null || refreshing}
              loading={pendingAll === 'collection'}
              onClick={() => addAll('collection')}
              size="sm"
              startIcon={<FolderPlus aria-hidden="true" size={14} />}
              variant="secondary"
            >
              Add all to collection
            </Button>
            <Button
              disabled={pendingAll !== null || refreshing}
              loading={pendingAll === 'playlist'}
              onClick={() => addAll('playlist')}
              size="sm"
              startIcon={<ListVideo aria-hidden="true" size={14} />}
              variant="secondary"
            >
              Add all to playlist
            </Button>
            <Button
              disabled={refreshing}
              onClick={() => setDialog({ kind: 'save-smart' })}
              size="sm"
              startIcon={<Save aria-hidden="true" size={14} />}
              variant="secondary"
            >
              Save as smart collection
            </Button>
          </>
        )}
      </div>

      {notice && (
        <Toast message={notice.message} onClose={() => setNotice(null)} tone={notice.tone} />
      )}

      {selectMode && selectionCount > 0 && (
        <div className="ctv-search-selectbar" role="region" aria-label="Selection actions">
          <span className="ctv-search-selectbar-count">{selectionCount} selected</span>
          <span className="ctv-channels-spacer" />
          <Button
            disabled={refreshing}
            onClick={() => addSelection('collection')}
            size="sm"
            startIcon={<FolderPlus aria-hidden="true" size={14} />}
            variant="secondary"
          >
            Add to collection
          </Button>
          <Button
            disabled={refreshing}
            onClick={() => addSelection('playlist')}
            size="sm"
            startIcon={<ListVideo aria-hidden="true" size={14} />}
            variant="secondary"
          >
            Add to playlist
          </Button>
          <Button
            onClick={clearSelection}
            size="sm"
            startIcon={<X aria-hidden="true" size={14} />}
            variant="ghost"
          >
            Clear selection
          </Button>
        </div>
      )}

      {refreshing && (
        <div className="ctv-collections-loading" role="status">
          <Spinner size={14} />
          <span>Refreshing…</span>
        </div>
      )}

      {!hasQuery && (
        <Card>
          <div className="ctv-collections-empty">Type a query to search across every media kind.</div>
        </Card>
      )}

      {hasQuery && state.status === 'error' && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{state.error}</span>
          <span className="ctv-channels-spacer" />
          <Button onClick={load} size="sm" variant="secondary">
            Retry
          </Button>
        </div>
      )}

      {hasQuery && state.status === 'loading' && (
        <div className="ctv-collections-loading" role="status">
          <Spinner size={18} />
          <span>Searching…</span>
        </div>
      )}

      {hasQuery && state.status === 'success' && state.results && totalMatches === 0 && (
        <Card>
          <div className="ctv-collections-empty">No results for “{query.trim()}”.</div>
        </Card>
      )}

      {hasQuery &&
        state.status === 'success' &&
        state.results &&
        GROUPS.map((group) => {
          const data = state.results![group.key];
          if (data.totalCount === 0 || data.items.length === 0) {
            return null;
          }

          const hasMore = group.slug !== null && data.totalCount > data.items.length;

          return (
            <section key={group.key}>
              <div className="ctv-media-section-header">
                <h2>{group.label}</h2>
                <span className="ctv-media-section-count">
                  {data.totalCount} result{data.totalCount === 1 ? '' : 's'}
                </span>
                <span className="ctv-channels-spacer" />
                {hasMore && (
                  <Button onClick={() => seeAll(group.slug!)} size="sm" variant="ghost">
                    See all
                  </Button>
                )}
              </div>
              <div className={`ctv-media-grid${refreshing ? ' ctv-media-grid-dim' : ''}`}>
                {data.items.map((item) => {
                  const key = itemKey(item);
                  const detailPath = mediaDetailPath(item);
                  return (
                    <MediaPosterCard
                      // While refreshing, the per-card Add-to menu is withheld (stale result set);
                      // card navigation stays live.
                      actions={
                        selectMode || refreshing ? undefined : (
                          <AddToMenu compact items={[item]} onDone={(message) => setNotice({ tone: 'ok', message })} />
                        )
                      }
                      item={item}
                      key={key}
                      // While `selectMode && refreshing`, the card must be fully inert: neither
                      // `onOpen` (which MediaPosterCard falls back to when `onToggleSelect` is
                      // undefined) nor `onToggleSelect` may fire, or a mid-select click would
                      // navigate away instead of no-op'ing. Outside select mode, `onOpen` stays
                      // live during a refresh (intended).
                      onOpen={!selectMode && detailPath ? () => navigateToPath(detailPath) : undefined}
                      onToggleSelect={canSelect ? toggleSelect : undefined}
                      selected={selectMode ? selected.has(key) : undefined}
                    />
                  );
                })}
              </div>
            </section>
          );
        })}

      <AddToCollectionDialog
        items={dialog?.kind === 'collection' ? dialog.items : []}
        onAdded={onAddedToSelectionTarget('collection')}
        onClose={() => setDialog(null)}
        open={dialog?.kind === 'collection'}
      />
      <AddToPlaylistDialog
        items={dialog?.kind === 'playlist' ? dialog.items : []}
        onAdded={onAddedToSelectionTarget('playlist')}
        onClose={() => setDialog(null)}
        open={dialog?.kind === 'playlist'}
      />
      <SaveAsSmartCollectionDialog
        onClose={() => setDialog(null)}
        onSaved={(name) => {
          setDialog(null);
          clearSelection();
          setNotice({ tone: 'ok', message: `Saved smart collection “${name}”` });
        }}
        open={dialog?.kind === 'save-smart'}
        query={query.trim()}
      />
    </div>
  );
}
