import { useCallback, useEffect, useRef, useState } from 'react';
import {
  ArrowDown,
  ArrowLeft,
  ArrowUp,
  Check,
  FolderTree,
  Info,
  ListVideo,
  Pencil,
  Plus,
  Search,
  Sparkles,
  Trash2,
  TriangleAlert
} from 'lucide-react';
import {
  Badge,
  Button,
  Card,
  ConfirmDialog,
  Dialog,
  IconButton,
  Input,
  Spinner,
  Switch
} from '../components';
import {
  addItemsToCollection,
  ApiError,
  createCollection,
  createSmartCollection,
  deleteCollection,
  deleteSmartCollection,
  getCollectionItems,
  getCollectionItemsWithMeta,
  getCollections,
  getLibraryBrowseItems,
  getSmartCollections,
  messageFromCollectionError,
  removeItemFromCollection,
  toAddItemsRequest,
  updateCollection,
  updateCollectionCustomOrder,
  updateSmartCollection,
  type LibraryBrowseItem,
  type MediaCollection,
  type SmartCollection
} from '../api';
import { TYPE_LABEL } from '../media/mediaKinds';
import { SmartCollectionDialog } from '../builder/SmartCollectionDialog';

type Tab = 'manual' | 'smart';

// Every kind that can be added to a manual collection from the picker.
const ADDABLE_TYPE_LIST: LibraryBrowseItem['mediaType'][] = [
  'Movie',
  'TelevisionShow',
  'TelevisionSeason',
  'Artist',
  'Episode',
  'MusicVideo',
  'Song',
  'OtherVideo',
  'Image',
  'RemoteStream'
];
const ADDABLE_TYPES = new Set<LibraryBrowseItem['mediaType']>(ADDABLE_TYPE_LIST);

// The default fan-out excludes seasons so a multi-season show doesn't flood the
// results with per-season rows (issue #180); seasons (and the other narrower kinds
// added for #211) stay reachable via the explicit media-kind filter below.
const DEFAULT_SEARCH_KINDS: LibraryBrowseItem['mediaType'][] = ['Movie', 'TelevisionShow', 'Artist'];

type MediaKindFilter = 'all' | LibraryBrowseItem['mediaType'];

const MEDIA_KIND_FILTERS: { label: string; value: MediaKindFilter }[] = [
  { label: 'All', value: 'all' },
  { label: 'Movies', value: 'Movie' },
  { label: 'Shows', value: 'TelevisionShow' },
  { label: 'Seasons', value: 'TelevisionSeason' },
  { label: 'Artists', value: 'Artist' },
  { label: TYPE_LABEL.Episode, value: 'Episode' },
  { label: TYPE_LABEL.MusicVideo, value: 'MusicVideo' },
  { label: TYPE_LABEL.Song, value: 'Song' },
  { label: TYPE_LABEL.OtherVideo, value: 'OtherVideo' },
  { label: TYPE_LABEL.Image, value: 'Image' },
  { label: TYPE_LABEL.RemoteStream, value: 'RemoteStream' }
];

function sortByName<T extends { name?: null | string }>(items: T[]): T[] {
  return [...items].sort((left, right) => (left.name ?? '').localeCompare(right.name ?? ''));
}

/* ---------- data hook ---------- */

interface CollectionsData {
  manual: MediaCollection[];
  smart: SmartCollection[];
}

type CollectionsState =
  | { data: CollectionsData; error: null; status: 'success' }
  | { data: null; error: string; status: 'error' }
  | { data: null; error: null; status: 'loading' };

function useCollectionsData() {
  const [state, setState] = useState<CollectionsState>({ data: null, error: null, status: 'loading' });
  const activeRef = useRef(true);

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

  // Fetch only; state updates happen in the async callbacks (never synchronously in the
  // effect body) so the initial 'loading' default stands until data resolves.
  const load = useCallback(() => {
    Promise.all([getCollections(), getSmartCollections()])
      .then(([manual, smart]) => {
        if (activeRef.current) {
          setState({ data: { manual, smart }, error: null, status: 'success' });
        }
      })
      .catch((error: unknown) => {
        if (activeRef.current) {
          setState({ data: null, error: messageFromCollectionError(error), status: 'error' });
        }
      });
  }, []);

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

  const refresh = useCallback(
    (quiet = false) => {
      if (!quiet) {
        setState({ data: null, error: null, status: 'loading' });
      }

      load();
    },
    [load]
  );

  return { refresh, state };
}

/* ---------- create / rename dialogs ---------- */

function NameDialog({
  busy,
  confirmLabel,
  error,
  initialName = '',
  onCancel,
  onSubmit,
  open,
  title
}: {
  busy: boolean;
  confirmLabel: string;
  error: string | null;
  initialName?: string;
  onCancel: () => void;
  onSubmit: (name: string) => void;
  open: boolean;
  title: string;
}) {
  // Parent remounts this via `key` on each open, so `initialName` seeds fresh state
  // without a reset effect.
  const [name, setName] = useState(initialName);
  const trimmed = name.trim();

  return (
    <Dialog
      footer={
        <>
          <Button disabled={busy} onClick={onCancel} variant="secondary">
            Cancel
          </Button>
          <Button disabled={busy || trimmed.length === 0} loading={busy} onClick={() => onSubmit(trimmed)} variant="primary">
            {confirmLabel}
          </Button>
        </>
      }
      onClose={onCancel}
      open={open}
      title={title}
      width={440}
    >
      <Input label="Name" onChange={(event) => setName(event.target.value)} placeholder="Collection name" value={name} />
      {error && (
        <span className="ctv-field-error" role="alert">
          {error}
        </span>
      )}
    </Dialog>
  );
}

/* ---------- add-items picker ---------- */

function AddItemsDialog({
  collection,
  onAdded,
  onClose,
  open
}: {
  collection: MediaCollection;
  onAdded: () => void;
  onClose: () => void;
  open: boolean;
}) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<LibraryBrowseItem[]>([]);
  const [searching, setSearching] = useState(false);
  const [selected, setSelected] = useState<Map<string, LibraryBrowseItem>>(() => new Map());
  const [error, setError] = useState<string | null>(null);
  const [adding, setAdding] = useState(false);
  const [kindFilter, setKindFilter] = useState<MediaKindFilter>('all');

  const runSearch = async (filter: MediaKindFilter = kindFilter) => {
    setSearching(true);
    setError(null);

    try {
      const trimmed = query.trim();
      const kinds = filter === 'all' ? DEFAULT_SEARCH_KINDS : [filter];
      const perKind = await Promise.all(
        kinds.map((mediaType) => getLibraryBrowseItems({ pageSize: 50, query: trimmed, mediaType }))
      );
      const merged = perKind
        .flatMap((result) => result.page ?? [])
        .filter((item) => ADDABLE_TYPES.has(item.mediaType))
        .sort((left, right) => left.title.localeCompare(right.title, undefined, { sensitivity: 'base' }));
      setResults(merged.slice(0, 50));
    } catch (searchError) {
      setError(messageFromCollectionError(searchError, 'Unable to search library'));
    } finally {
      setSearching(false);
    }
  };

  const selectKindFilter = (filter: MediaKindFilter) => {
    setKindFilter(filter);
    void runSearch(filter);
  };

  const toggle = (item: LibraryBrowseItem) => {
    const key = `${item.mediaType}:${item.id}`;
    setSelected((current) => {
      const next = new Map(current);

      if (next.has(key)) {
        next.delete(key);
      } else {
        next.set(key, item);
      }

      return next;
    });
  };

  const submit = async () => {
    if (selected.size === 0) {
      return;
    }

    setAdding(true);
    setError(null);

    try {
      await addItemsToCollection(collection.id, toAddItemsRequest([...selected.values()]));
      onAdded();
      onClose();
    } catch (addError) {
      setError(messageFromCollectionError(addError, 'Unable to add items'));
    } finally {
      setAdding(false);
    }
  };

  return (
    <Dialog
      footer={
        <>
          <Button disabled={adding} onClick={onClose} variant="secondary">
            Cancel
          </Button>
          <Button
            disabled={adding || selected.size === 0}
            loading={adding}
            onClick={() => void submit()}
            startIcon={<Plus aria-hidden="true" size={14} />}
            variant="primary"
          >
            Add {selected.size > 0 ? selected.size : ''} item{selected.size === 1 ? '' : 's'}
          </Button>
        </>
      }
      onClose={onClose}
      open={open}
      title={`Add items to ${collection.name ?? 'collection'}`}
      width={620}
    >
      <form
        onSubmit={(event) => {
          event.preventDefault();
          void runSearch();
        }}
        style={{ display: 'flex', gap: 8 }}
      >
        <Input
          leadingIcon={<Search aria-hidden="true" size={14} />}
          onChange={(event) => setQuery(event.target.value)}
          placeholder="Search movies, shows, seasons, artists…"
          value={query}
        />
        <Button loading={searching} size="sm" type="submit" variant="secondary">
          Search
        </Button>
      </form>
      <div className="ctv-collections-picker-filters" role="group" aria-label="Filter by media kind">
        {MEDIA_KIND_FILTERS.map((filter) => (
          <button
            aria-pressed={kindFilter === filter.value}
            className={`ctv-collections-picker-filter ctv-press${kindFilter === filter.value ? ' ctv-collections-picker-filter-active' : ''}`}
            key={filter.value}
            onClick={() => selectKindFilter(filter.value)}
            type="button"
          >
            {filter.label}
          </button>
        ))}
      </div>
      <p className="ctv-collections-picker-note">
        &ldquo;All&rdquo; searches movies, shows and artists; pick a specific kind to add seasons, episodes,
        music videos, songs, other videos, images or remote streams.
      </p>
      {error && (
        <span className="ctv-field-error" role="alert">
          {error}
        </span>
      )}
      <div className="ctv-collections-picker-results">
        {results.length === 0 && !searching ? (
          <div className="ctv-collections-picker-empty">No results — try a search above.</div>
        ) : (
          results.map((item) => {
            const key = `${item.mediaType}:${item.id}`;
            const isSelected = selected.has(key);

            return (
              <button
                aria-pressed={isSelected}
                className={`ctv-collections-picker-row ctv-press${isSelected ? ' ctv-collections-picker-row-active' : ''}`}
                key={key}
                onClick={() => toggle(item)}
                type="button"
              >
                <span className="ctv-collections-picker-check">
                  {isSelected ? <Check aria-hidden="true" size={14} /> : null}
                </span>
                <span className="ctv-collections-picker-row-title">{item.title}</span>
                <Badge tone="neutral">{TYPE_LABEL[item.mediaType]}</Badge>
              </button>
            );
          })
        )}
      </div>
    </Dialog>
  );
}

/* ---------- manual collection items view ---------- */

function ManualItemsView({
  collection,
  onBack
}: {
  collection: MediaCollection;
  onBack: () => void;
}) {
  const PAGE_SIZE = 100;
  const [items, setItems] = useState<LibraryBrowseItem[]>([]);
  const [total, setTotal] = useState(0);
  const [loading, setLoading] = useState(true);
  const [loadingMore, setLoadingMore] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [pickerOpen, setPickerOpen] = useState(false);
  const [removing, setRemoving] = useState<number | null>(null);
  const [reordering, setReordering] = useState(false);
  const [reorderItems, setReorderItems] = useState<LibraryBrowseItem[] | null>(null);
  const [reorderLoading, setReorderLoading] = useState(false);
  const [reorderSaving, setReorderSaving] = useState(false);
  const [conflictOpen, setConflictOpen] = useState(false);
  const activeRef = useRef(true);
  // Concurrency ETag (issue #253): captured from the first page of the reorder-mode items GET,
  // sent as If-Match on the custom-order save.
  const etagRef = useRef<string | null>(null);

  // No synchronous setState here: `loading` starts true and flips false in `finally`, so
  // this is safe to call from an effect. Reloads (after add/remove) reset to the first page.
  const load = useCallback(() => {
    getCollectionItems(collection.id, 0, PAGE_SIZE)
      .then((result) => {
        if (activeRef.current) {
          setItems(result.page ?? []);
          setTotal(result.totalCount ?? 0);
          setError(null);
        }
      })
      .catch((loadError: unknown) => {
        if (activeRef.current) {
          setError(messageFromCollectionError(loadError, 'Unable to load collection items'));
        }
      })
      .finally(() => {
        if (activeRef.current) {
          setLoading(false);
        }
      });
  }, [collection.id]);

  const loadMore = async () => {
    const nextPage = Math.floor(items.length / PAGE_SIZE);
    setLoadingMore(true);
    try {
      const result = await getCollectionItems(collection.id, nextPage, PAGE_SIZE);
      if (activeRef.current) {
        setItems((prev) => [...prev, ...(result.page ?? [])]);
        setTotal(result.totalCount ?? 0);
      }
    } catch (moreError) {
      setError(messageFromCollectionError(moreError, 'Unable to load more items'));
    } finally {
      setLoadingMore(false);
    }
  };

  useEffect(() => {
    activeRef.current = true;
    load();

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

  const remove = async (item: LibraryBrowseItem) => {
    const mediaItemId = item.mediaItemId ?? item.id;
    setRemoving(mediaItemId);

    try {
      await removeItemFromCollection(collection.id, mediaItemId);
      load();
    } catch (removeError) {
      setError(messageFromCollectionError(removeError, 'Unable to remove item'));
    } finally {
      setRemoving(null);
    }
  };

  // The custom-order PUT replaces the order wholesale from the submitted array, so a
  // partial list would silently drop the items left out of it — every page must be loaded
  // before reorder mode is entered. If any page fails to load, bail out without entering
  // reorder mode (the error surfaces via the normal `error` banner).
  const enterReorder = async () => {
    setReorderLoading(true);
    setError(null);

    try {
      const first = await getCollectionItemsWithMeta(collection.id, 0, PAGE_SIZE);
      let all = first.data.page ?? [];
      const totalCount = first.data.totalCount ?? all.length;
      let pageNum = 1;

      while (all.length < totalCount) {
        const next = await getCollectionItems(collection.id, pageNum, PAGE_SIZE);
        const nextPage = next.page ?? [];

        if (nextPage.length === 0) {
          break;
        }

        all = [...all, ...nextPage];
        pageNum += 1;
      }

      if (activeRef.current) {
        etagRef.current = first.etag;
        setReorderItems(all);
        setReordering(true);
      }
    } catch (reorderLoadError) {
      if (activeRef.current) {
        setError(messageFromCollectionError(reorderLoadError, 'Unable to load items for reordering'));
      }
    } finally {
      if (activeRef.current) {
        setReorderLoading(false);
      }
    }
  };

  const moveItem = (index: number, direction: -1 | 1) => {
    setReorderItems((current) => {
      if (!current) {
        return current;
      }

      const target = index + direction;
      if (target < 0 || target >= current.length) {
        return current;
      }

      const next = [...current];
      [next[index], next[target]] = [next[target], next[index]];
      return next;
    });
  };

  const cancelReorder = () => {
    setReordering(false);
    setReorderItems(null);
    setError(null);
  };

  const saveOrder = async () => {
    if (!reorderItems) {
      return;
    }

    setReorderSaving(true);
    setError(null);

    try {
      const mediaItemIds = reorderItems.map((item) => item.mediaItemId ?? item.id);
      await updateCollectionCustomOrder(collection.id, mediaItemIds, etagRef.current);

      if (activeRef.current) {
        setReordering(false);
        setReorderItems(null);
        load();
      }
    } catch (saveError) {
      if (activeRef.current) {
        if (saveError instanceof ApiError && saveError.status === 412) {
          // Another edit landed since we loaded the reorder view — force a reload rather than
          // overwriting it (#253).
          setConflictOpen(true);
        } else {
          setError(messageFromCollectionError(saveError, 'Unable to save order'));
        }
      }
    } finally {
      if (activeRef.current) {
        setReorderSaving(false);
      }
    }
  };

  const reloadAfterConflict = () => {
    setConflictOpen(false);
    setError(null);
    setReordering(false);
    setReorderItems(null);
    void enterReorder();
  };

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <Button onClick={onBack} size="sm" startIcon={<ArrowLeft aria-hidden="true" size={14} />} variant="ghost">
          All collections
        </Button>
        <span className="ctv-collections-detail-title">{collection.name}</span>
        <span className="ctv-channels-spacer" />
        {collection.useCustomPlaybackOrder && !reordering && (
          <Button
            disabled={reorderLoading}
            loading={reorderLoading}
            onClick={() => void enterReorder()}
            size="sm"
            startIcon={<ArrowUp aria-hidden="true" size={14} />}
            variant="secondary"
          >
            Reorder
          </Button>
        )}
        <Button
          disabled={reordering || reorderLoading}
          onClick={() => setPickerOpen(true)}
          size="sm"
          startIcon={<Plus aria-hidden="true" size={14} />}
        >
          Add items
        </Button>
      </div>

      {error && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{error}</span>
        </div>
      )}

      <Card padded={false}>
        {reordering && reorderItems ? (
          reorderItems.length === 0 ? (
            <div className="ctv-collections-empty">No items in this collection.</div>
          ) : (
            reorderItems.map((item, index) => (
              <div
                className="ctv-settings-flush-row"
                key={`${item.mediaType}:${item.id}`}
                style={index === 0 ? { borderTop: 'none' } : undefined}
              >
                <ListVideo aria-hidden="true" color="var(--text-disabled)" size={15} />
                <span className="ctv-settings-flush-row-main ctv-settings-flush-row-title">{item.title}</span>
                <Badge tone="neutral">{TYPE_LABEL[item.mediaType]}</Badge>
                <IconButton
                  disabled={index === 0 || reorderSaving}
                  onClick={() => moveItem(index, -1)}
                  size="sm"
                  title={`Move ${item.title} up`}
                  variant="ghost"
                >
                  <ArrowUp aria-hidden="true" size={14} />
                </IconButton>
                <IconButton
                  disabled={index === reorderItems.length - 1 || reorderSaving}
                  onClick={() => moveItem(index, 1)}
                  size="sm"
                  title={`Move ${item.title} down`}
                  variant="ghost"
                >
                  <ArrowDown aria-hidden="true" size={14} />
                </IconButton>
              </div>
            ))
          )
        ) : loading ? (
          <div className="ctv-collections-loading">
            <Spinner size={18} />
            <span>Loading items…</span>
          </div>
        ) : items.length === 0 ? (
          <div className="ctv-collections-empty">No items in this collection.</div>
        ) : (
          items.map((item, index) => (
            <div
              className="ctv-settings-flush-row"
              key={`${item.mediaType}:${item.id}`}
              style={index === 0 ? { borderTop: 'none' } : undefined}
            >
              <ListVideo aria-hidden="true" color="var(--text-disabled)" size={15} />
              <span className="ctv-settings-flush-row-main ctv-settings-flush-row-title">{item.title}</span>
              <Badge tone="neutral">{TYPE_LABEL[item.mediaType]}</Badge>
              <IconButton
                onClick={() => void remove(item)}
                size="sm"
                title={`Remove ${item.title}`}
                variant="ghost"
              >
                {removing === (item.mediaItemId ?? item.id) ? (
                  <Spinner size={14} />
                ) : (
                  <Trash2 aria-hidden="true" size={14} />
                )}
              </IconButton>
            </div>
          ))
        )}
      </Card>

      {reordering ? (
        <div className="ctv-collections-loadmore">
          <span className="ctv-collections-count">{reorderItems?.length ?? 0} items</span>
          <div style={{ display: 'flex', gap: 8 }}>
            <Button disabled={reorderSaving} onClick={cancelReorder} size="sm" variant="ghost">
              Cancel
            </Button>
            <Button disabled={reorderSaving} loading={reorderSaving} onClick={() => void saveOrder()} size="sm" variant="primary">
              Save order
            </Button>
          </div>
        </div>
      ) : (
        !loading &&
        items.length < total && (
          <div className="ctv-collections-loadmore">
            <span className="ctv-collections-count">
              Showing {items.length} of {total}
            </span>
            <Button disabled={loadingMore} onClick={() => void loadMore()} size="sm" variant="ghost">
              {loadingMore ? <Spinner size={14} /> : 'Load more'}
            </Button>
          </div>
        )
      )}

      <AddItemsDialog
        collection={collection}
        key={`add-${pickerOpen}`}
        onAdded={load}
        onClose={() => setPickerOpen(false)}
        open={pickerOpen}
      />

      <ConfirmDialog
        cancelLabel="Keep editing"
        confirmLabel="Reload"
        message="This collection's order was changed elsewhere since you opened it. Reload to get the latest — your unsaved changes will be discarded."
        onCancel={() => setConflictOpen(false)}
        onConfirm={reloadAfterConflict}
        open={conflictOpen}
        title="Order changed elsewhere"
        tone="danger"
      />
    </div>
  );
}

/* ---------- screen ---------- */

export function CollectionsScreen() {
  const { refresh, state } = useCollectionsData();
  const [tab, setTab] = useState<Tab>('manual');
  const [selected, setSelected] = useState<MediaCollection | null>(null);

  // dialogs
  const [createOpen, setCreateOpen] = useState(false);
  const [renameTarget, setRenameTarget] = useState<MediaCollection | null>(null);
  const [smartTarget, setSmartTarget] = useState<{ collection: SmartCollection | null; open: boolean }>({
    collection: null,
    open: false
  });
  const [deleteTarget, setDeleteTarget] = useState<
    { kind: 'manual'; item: MediaCollection } | { kind: 'smart'; item: SmartCollection } | null
  >(null);

  const [dialogBusy, setDialogBusy] = useState(false);
  const [dialogError, setDialogError] = useState<string | null>(null);
  const [rowError, setRowError] = useState<string | null>(null);
  const [togglingId, setTogglingId] = useState<number | null>(null);

  if (state.status === 'loading') {
    return (
      <div className="ctv-collections-loading" role="status">
        <Spinner size={18} />
        <span>Loading collections…</span>
      </div>
    );
  }

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

  if (selected) {
    // Keep the selected reference fresh across refreshes (e.g. after a rename elsewhere).
    const current = state.data.manual.find((collection) => collection.id === selected.id) ?? selected;
    return <ManualItemsView collection={current} onBack={() => setSelected(null)} />;
  }

  const manual = sortByName(state.data.manual);
  const smart = sortByName(state.data.smart);

  const runDialog = async (operation: () => Promise<void>, onSuccess: () => void) => {
    setDialogBusy(true);
    setDialogError(null);

    try {
      await operation();
      onSuccess();
      refresh(true);
    } catch (error) {
      setDialogError(messageFromCollectionError(error, 'Operation failed'));
    } finally {
      setDialogBusy(false);
    }
  };

  const toggleCustomOrder = async (collection: MediaCollection, next: boolean) => {
    setRowError(null);
    setTogglingId(collection.id);

    try {
      await updateCollection(collection.id, { name: collection.name, useCustomPlaybackOrder: next });
      refresh(true);
    } catch (error) {
      setRowError(messageFromCollectionError(error, 'Unable to update collection'));
    } finally {
      setTogglingId(null);
    }
  };

  const confirmDelete = () => {
    if (!deleteTarget) {
      return;
    }

    const target = deleteTarget;
    void runDialog(
      () =>
        target.kind === 'manual'
          ? deleteCollection(target.item.id)
          : deleteSmartCollection(target.item.id),
      () => setDeleteTarget(null)
    );
  };

  return (
    <div className="ctv-collections">
      <div className="ctv-channels-actionbar">
        <div aria-label="Collection type" className="ctv-segmented" role="group">
          <button aria-pressed={tab === 'manual'} onClick={() => setTab('manual')} type="button">
            Manual <code>{manual.length}</code>
          </button>
          <button aria-pressed={tab === 'smart'} onClick={() => setTab('smart')} type="button">
            Smart <code>{smart.length}</code>
          </button>
        </div>
        <span className="ctv-channels-spacer" />
        {tab === 'manual' ? (
          <Button
            onClick={() => {
              setDialogError(null);
              setCreateOpen(true);
            }}
            size="sm"
            startIcon={<Plus aria-hidden="true" size={14} />}
          >
            New collection
          </Button>
        ) : (
          <Button
            onClick={() => {
              setDialogError(null);
              setSmartTarget({ collection: null, open: true });
            }}
            size="sm"
            startIcon={<Plus aria-hidden="true" size={14} />}
          >
            New smart collection
          </Button>
        )}
      </div>

      {rowError && (
        <div className="ctv-channels-error" role="alert">
          <TriangleAlert aria-hidden="true" size={15} />
          <span>{rowError}</span>
        </div>
      )}

      {tab === 'manual' && (
        <Card padded={false}>
          {manual.length === 0 ? (
            <div className="ctv-collections-empty">No manual collections yet.</div>
          ) : (
            manual.map((collection, index) => (
              <div
                className="ctv-settings-flush-row"
                key={collection.id}
                style={index === 0 ? { borderTop: 'none' } : undefined}
              >
                <FolderTree aria-hidden="true" color="var(--ctv-accent)" size={15} />
                <button
                  className="ctv-settings-flush-row-main ctv-settings-flush-row-title ctv-collections-linkbtn"
                  onClick={() => setSelected(collection)}
                  type="button"
                >
                  {collection.name}
                </button>
                <label className="ctv-collections-order-toggle" title="Use custom playback order">
                  <Switch
                    checked={collection.useCustomPlaybackOrder}
                    disabled={togglingId === collection.id}
                    onChange={(next) => void toggleCustomOrder(collection, next)}
                    size="sm"
                  />
                  <span>Custom order</span>
                </label>
                <IconButton onClick={() => setSelected(collection)} size="sm" title="Manage items" variant="ghost">
                  <ListVideo aria-hidden="true" size={14} />
                </IconButton>
                <IconButton
                  onClick={() => {
                    setDialogError(null);
                    setRenameTarget(collection);
                  }}
                  size="sm"
                  title="Rename"
                  variant="ghost"
                >
                  <Pencil aria-hidden="true" size={14} />
                </IconButton>
                <IconButton
                  onClick={() => setDeleteTarget({ item: collection, kind: 'manual' })}
                  size="sm"
                  title="Delete"
                  variant="ghost"
                >
                  <Trash2 aria-hidden="true" size={14} />
                </IconButton>
              </div>
            ))
          )}
        </Card>
      )}

      {tab === 'smart' && (
        <Card padded={false}>
          {smart.length === 0 ? (
            <div className="ctv-collections-empty">No smart collections yet.</div>
          ) : (
            smart.map((collection, index) => (
              <div
                className="ctv-settings-flush-row"
                key={collection.id}
                style={index === 0 ? { borderTop: 'none' } : undefined}
              >
                <Sparkles aria-hidden="true" color="var(--ctv-accent)" size={15} />
                <div className="ctv-settings-flush-row-main">
                  <div className="ctv-settings-flush-row-title">{collection.name}</div>
                  <div className="ctv-collections-query">{collection.query}</div>
                </div>
                <IconButton
                  onClick={() => {
                    setDialogError(null);
                    setSmartTarget({ collection, open: true });
                  }}
                  size="sm"
                  title="Edit"
                  variant="ghost"
                >
                  <Pencil aria-hidden="true" size={14} />
                </IconButton>
                <IconButton
                  onClick={() => setDeleteTarget({ item: collection, kind: 'smart' })}
                  size="sm"
                  title="Delete"
                  variant="ghost"
                >
                  <Trash2 aria-hidden="true" size={14} />
                </IconButton>
              </div>
            ))
          )}
        </Card>
      )}

      <ClassicNote />

      {/* create manual */}
      <NameDialog
        busy={dialogBusy}
        confirmLabel="Create"
        error={createOpen ? dialogError : null}
        key={`create-${createOpen}`}
        onCancel={() => setCreateOpen(false)}
        onSubmit={(name) => void runDialog(() => createCollection({ name }).then(() => undefined), () => setCreateOpen(false))}
        open={createOpen}
        title="New collection"
      />

      {/* rename manual */}
      <NameDialog
        busy={dialogBusy}
        confirmLabel="Save"
        error={renameTarget ? dialogError : null}
        initialName={renameTarget?.name ?? ''}
        key={`rename-${renameTarget?.id ?? 'none'}`}
        onCancel={() => setRenameTarget(null)}
        onSubmit={(name) =>
          renameTarget &&
          void runDialog(
            () =>
              updateCollection(renameTarget.id, {
                name,
                useCustomPlaybackOrder: renameTarget.useCustomPlaybackOrder
              }).then(() => undefined),
            () => setRenameTarget(null)
          )
        }
        open={renameTarget !== null}
        title="Rename collection"
      />

      {/* create / edit smart */}
      <SmartCollectionDialog
        busy={dialogBusy}
        error={dialogError}
        key={`smart-${smartTarget.open}-${smartTarget.collection?.id ?? 'new'}`}
        initial={
          smartTarget.collection
            ? { name: smartTarget.collection.name ?? '', query: smartTarget.collection.query ?? '' }
            : null
        }
        onCancel={() => setSmartTarget({ collection: null, open: false })}
        onSubmit={(values) => {
          const editing = smartTarget.collection;
          void runDialog(
            () =>
              (editing
                ? updateSmartCollection(editing.id, values)
                : createSmartCollection(values)
              ).then(() => undefined),
            () => setSmartTarget({ collection: null, open: false })
          );
        }}
        open={smartTarget.open}
      />

      <ConfirmDialog
        busy={dialogBusy}
        confirmLabel="Delete"
        message={
          deleteTarget ? (
            <>
              <span>{`Delete "${deleteTarget.item.name ?? 'this collection'}"? This cannot be undone.`}</span>
              {dialogError && (
                <span className="ctv-field-error" role="alert">
                  {dialogError}
                </span>
              )}
            </>
          ) : (
            ''
          )
        }
        onCancel={() => {
          setDeleteTarget(null);
          setDialogError(null);
        }}
        onConfirm={confirmDelete}
        open={deleteTarget !== null}
        title={deleteTarget?.kind === 'smart' ? 'Delete smart collection' : 'Delete collection'}
        tone="danger"
      />
    </div>
  );
}

function ClassicNote() {
  return (
    <div className="ctv-settings-callout">
      <Info aria-hidden="true" size={14} />
      <span>
        Multi-collections, rerun collections and playlists aren't available here yet — manage them in the Classic UI
        for now.
      </span>
    </div>
  );
}
