import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
  DndContext,
  PointerSensor,
  closestCenter,
  useSensor,
  useSensors,
  type DragEndEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
  ArrowDown,
  ArrowUp,
  ExternalLink,
  GripVertical,
  ListPlus,
  Pencil,
  Pin,
  Play,
  Plus,
  RefreshCw,
  RotateCcw,
  Trash2,
  Upload,
  X,
  Youtube,
} from "lucide-react";
import { api, type Playlist, type Video } from "../lib/api";
import { formatDuration } from "../lib/format";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
import { getAccountRaw, LS, readAccountMerged, setAccountRaw, writeAccount } from "../lib/storage";
import { useUndoable } from "../lib/useUndoable";
const PlayerModal = lazy(() => import("./PlayerModal"));
import UndoToolbar from "./UndoToolbar";
import { useConfirm } from "./ConfirmProvider";

type SortKey = "manual" | "title" | "duration" | "channel";
type SortDir = "asc" | "desc";

function comparator(key: SortKey, dir: SortDir): ((a: Video, b: Video) => number) | null {
  if (key === "manual") return null;
  const sign = dir === "asc" ? 1 : -1;
  if (key === "duration")
    return (a, b) => {
      const av = a.duration_seconds;
      const bv = b.duration_seconds;
      if (av == null && bv == null) return 0;
      if (av == null) return 1; // nulls always last, both directions
      if (bv == null) return -1;
      return sign * (av - bv);
    };
  return (a, b) => {
    const av = (key === "channel" ? a.channel_title : a.title) ?? "";
    const bv = (key === "channel" ? b.channel_title : b.title) ?? "";
    return sign * av.localeCompare(bv);
  };
}

// Sort a playlist's items. When `group` is on, items are grouped by channel (groups ordered
// by channel name in the chosen direction) and the chosen sort is applied within each group;
// with key "manual" the within-group order is left as-is. Returns the same array reference
// when nothing would change, so it doesn't create a spurious undo entry.
function sortItems(items: Video[], key: SortKey, dir: SortDir, group: boolean): Video[] {
  const cmp = comparator(key, dir);
  if (!group) return cmp ? [...items].sort(cmp) : items;
  const groups = new Map<string, Video[]>();
  for (const v of items) {
    const k = v.channel_title ?? "";
    const g = groups.get(k);
    if (g) g.push(v);
    else groups.set(k, [v]);
  }
  const sign = dir === "asc" ? 1 : -1;
  const keys = [...groups.keys()].sort((a, b) => sign * a.localeCompare(b));
  const out: Video[] = [];
  for (const k of keys) {
    const g = groups.get(k)!;
    out.push(...(cmp ? [...g].sort(cmp) : g));
  }
  return out;
}

const idsKey = (items: Video[]) =>
  items
    .map((v) => v.id)
    .sort()
    .join(",");

type PlSort = { key: "custom" | "name" | "count" | "duration"; dir: SortDir; dirtyFirst: boolean };
const PL_SORT_DEFAULT: PlSort = { key: "custom", dir: "asc", dirtyFirst: false };

function Row({
  video,
  index,
  readOnly,
  onPlay,
  onRemove,
}: {
  video: Video;
  index: number;
  readOnly?: boolean;
  onPlay: () => void;
  onRemove: () => void;
}) {
  const { t } = useTranslation();
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
    useSortable({ id: video.id, disabled: readOnly });
  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.6 : 1,
  };
  return (
    <div
      ref={setNodeRef}
      style={style}
      className="flex items-center gap-2.5 p-2 rounded-lg border border-border bg-card"
    >
      {readOnly ? (
        <span className="w-4" />
      ) : (
        <button
          {...attributes}
          {...listeners}
          className="text-muted hover:text-fg cursor-grab active:cursor-grabbing"
          aria-label="reorder"
        >
          <GripVertical className="w-4 h-4" />
        </button>
      )}
      <span className="text-xs text-muted w-4 text-center tabular-nums">{index + 1}</span>
      <button
        onClick={onPlay}
        aria-label={`${t("card.play")} — ${video.title}`}
        className="relative w-[62px] h-[35px] rounded overflow-hidden bg-surface shrink-0 group"
      >
        {video.thumbnail_url && (
          <img src={video.thumbnail_url} alt="" className="w-full h-full object-cover" />
        )}
        <span className="absolute inset-0 grid place-items-center bg-black/40 opacity-0 group-hover:opacity-100 transition">
          <Play className="w-4 h-4 text-white fill-current" />
        </span>
      </button>
      <button onClick={onPlay} className="min-w-0 flex-1 text-left">
        <div className="text-[13px] text-fg truncate">{video.title}</div>
        <div className="text-[11px] text-muted truncate">{video.channel_title}</div>
      </button>
      {video.duration_seconds != null ? (
        <span className="text-[11px] text-muted tabular-nums">
          {formatDuration(video.duration_seconds)}
        </span>
      ) : video.live_status === "live" || video.live_status === "upcoming" ? (
        <span
          className={`text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded ${
            video.live_status === "live"
              ? "bg-red-500/90 text-white"
              : "bg-card border border-border text-muted"
          }`}
        >
          {t(`card.${video.live_status}`)}
        </span>
      ) : null}
      {!readOnly && (
        <button
          onClick={onRemove}
          title="remove"
          className="text-muted hover:text-fg p-1"
          aria-label="remove from playlist"
        >
          <X className="w-4 h-4" />
        </button>
      )}
    </div>
  );
}

export default function Playlists({ canWrite }: { canWrite: boolean }) {
  const { t } = useTranslation();
  const qc = useQueryClient();
  const confirm = useConfirm();
  // Persist the selected playlist so F5 keeps it (the de-URL refactor dropped it from the URL).
  const [selectedId, setSelectedId] = useState<number | null>(() => {
    const s = getAccountRaw(LS.playlist);
    return s ? Number(s) : null;
  });
  useEffect(() => {
    if (selectedId != null) setAccountRaw(LS.playlist, String(selectedId));
  }, [selectedId]);
  const selectedRef = useRef<HTMLButtonElement | null>(null);
  const scrolledRef = useRef(false);
  // How the left playlist rail is ordered (persisted).
  const [plSort, setPlSort] = useState<PlSort>(() => readAccountMerged(LS.plSort, PL_SORT_DEFAULT));
  useEffect(() => {
    writeAccount(LS.plSort, plSort);
  }, [plSort]);
  const [newName, setNewName] = useState("");
  const [renaming, setRenaming] = useState(false);
  const [renameValue, setRenameValue] = useState("");
  // The item order is undoable: drag, sort and group all go through `order.set`, so each is
  // reversible (buttons + Ctrl+Z/Y). onApply persists the new order to the server.
  const order = useUndoable<Video[]>([], {
    onApply: (next) => {
      if (selectedId == null) return;
      api.reorderPlaylist(selectedId, next.map((v) => v.id)).then(() => {
        // Refresh the sidebar (cover may change) and the detail (so the now-dirty state,
        // and the Reset/Unsynced indicators, show without an F5). The membership guard
        // keeps the refetch from wiping the undo history on a pure reorder.
        qc.invalidateQueries({ queryKey: ["playlists"] });
        qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
      });
    },
  });
  const items = order.value;
  const [sortKey, setSortKey] = useState<SortKey>("manual");
  const [sortDir, setSortDir] = useState<SortDir>("asc");
  const [groupBy, setGroupBy] = useState(false);
  // Tracks the membership (id set) currently loaded into `order`, so a refetch that only
  // re-confirms our own reorder doesn't wipe the undo history — we reset history only when
  // the actual set of items changes (different playlist, or an add/remove).
  const lastSetRef = useRef<string>("");
  // The open player, as an index into `items` (the queue); null = closed.
  const [playingIndex, setPlayingIndex] = useState<number | null>(null);

  const listQuery = useQuery({
    queryKey: ["playlists"],
    queryFn: () => api.playlists(),
    refetchOnWindowFocus: true,
  });
  const playlists = listQuery.data ?? [];

  // Keep the selection valid: default to the first playlist, and if the selected one
  // disappears (e.g. just deleted) drop to the next available, or clear when none remain.
  useEffect(() => {
    // Wait for the first load before adjusting the selection — otherwise the empty
    // placeholder list would null the restored selection and we'd fall back to the first.
    if (!listQuery.data) return;
    if (!playlists.length) {
      if (selectedId != null) setSelectedId(null);
      return;
    }
    if (selectedId == null || !playlists.some((p) => p.id === selectedId)) {
      setSelectedId(playlists[0].id);
    }
  }, [playlists, selectedId, listQuery.data]);

  const detailQuery = useQuery({
    queryKey: ["playlist", selectedId],
    queryFn: () => api.playlist(selectedId as number),
    enabled: selectedId != null,
    // Refetch when returning to the tab so a change made elsewhere (another tab/device)
    // shows up — including the player's live N / M count, which reads the queue length.
    refetchOnWindowFocus: true,
  });
  const detail = detailQuery.data;

  useEffect(() => {
    if (!detail) return;
    const key = idsKey(detail.items);
    if (key !== lastSetRef.current) {
      lastSetRef.current = key;
      order.reset(detail.items);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [detail]);

  // Reset the sort controls when switching playlists (the order itself is per-playlist).
  useEffect(() => {
    setSortKey("manual");
    setSortDir("asc");
    setGroupBy(false);
  }, [selectedId]);

  function applySort(key: SortKey, dir: SortDir, group: boolean) {
    order.set(sortItems(items, key, dir, group));
  }
  const undo = () => {
    setSortKey("manual");
    setGroupBy(false);
    order.undo();
  };
  const redo = () => {
    setSortKey("manual");
    setGroupBy(false);
    order.redo();
  };

  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 4 } })
  );

  // Watch later is a built-in playlist: show a localized name and no rename/delete.
  const plName = (p: { kind: string; name: string }) =>
    p.kind === "watch_later" ? t("playlists.watchLater") : p.name;

  // The left rail in the chosen order. "custom" keeps the server position order (Array.sort
  // is stable); "dirty first" floats playlists with unpushed edits to the top.
  const sortedPlaylists = useMemo(() => {
    const sign = plSort.dir === "asc" ? 1 : -1;
    return [...playlists].sort((a, b) => {
      if (plSort.dirtyFirst && a.dirty !== b.dirty) return a.dirty ? -1 : 1;
      switch (plSort.key) {
        case "name":
          return sign * plName(a).localeCompare(plName(b));
        case "count":
          return sign * (a.item_count - b.item_count);
        case "duration":
          return sign * ((a.total_duration_seconds ?? 0) - (b.total_duration_seconds ?? 0));
        default:
          return 0; // custom: server order
      }
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [playlists, plSort]);

  // Scroll the restored selection into view once after the rail first renders.
  useEffect(() => {
    if (!scrolledRef.current && selectedRef.current) {
      selectedRef.current.scrollIntoView({ block: "nearest" });
      scrolledRef.current = true;
    }
  }, [sortedPlaylists]);

  const builtin = detail?.kind === "watch_later";
  // YouTube-mirrored playlists can be edited locally and synced back (the read-sync skips a
  // dirty mirror so it won't clobber unpushed edits); the YT-link button marks their origin.
  const editable = !builtin;
  const linked = editable && !!detail?.yt_playlist_id;
  const [syncing, setSyncing] = useState(false);
  const [pushing, setPushing] = useState(false);
  const [reverting, setReverting] = useState(false);

  async function revertToYoutube() {
    if (selectedId == null || !detail || reverting) return;
    const ok = await confirm({
      title: t("playlists.revertTitle"),
      message: t("playlists.revertMsg"),
      confirmLabel: t("playlists.revertConfirm"),
      danger: true,
    });
    if (!ok) return;
    setReverting(true);
    try {
      await api.revertPlaylist(selectedId);
      refreshAll();
      notify({ level: "success", message: t("playlists.revertDone", { name: plName(detail) }) });
    } catch (e) {
      notifyYouTubeActionError(e, t("playlists.revertFailed"));
    } finally {
      setReverting(false);
    }
  }

  async function pushToYoutube() {
    if (selectedId == null || !detail || pushing) return;
    setPushing(true);
    try {
      const plan = await api.playlistPushPlan(selectedId);
      if (plan.action === "update" && !plan.to_insert && !plan.to_delete && !plan.to_reorder) {
        notify({ level: "info", message: t("playlists.pushUpToDate", { name: plName(detail) }) });
        return;
      }
      if (!plan.affordable) {
        notify({
          level: "warning",
          message: t("playlists.pushNoQuota", {
            left: plan.remaining_today,
            units: plan.units_estimate,
          }),
        });
        return;
      }
      let message =
        plan.action === "create"
          ? t("playlists.pushPlanCreate", {
              count: plan.to_insert,
              units: plan.units_estimate,
              left: plan.remaining_today,
            })
          : t("playlists.pushPlanUpdate", {
              insert: plan.to_insert,
              del: plan.to_delete,
              reorder: plan.to_reorder,
              units: plan.units_estimate,
              left: plan.remaining_today,
            });
      if (plan.yt_extra > 0) message += " " + t("playlists.pushDiverged", { count: plan.yt_extra });
      const ok = await confirm({
        title: t("playlists.pushTitle"),
        message,
        confirmLabel: t("playlists.pushConfirm"),
      });
      if (!ok) return;
      const r = await api.pushPlaylist(selectedId);
      refreshAll();
      if (r.failures.length) {
        notify({ level: "warning", message: t("playlists.pushPartial", { count: r.failures.length }) });
      } else {
        notify({ level: "success", message: t("playlists.pushDone", { name: plName(detail) }) });
      }
    } catch (e) {
      notifyYouTubeActionError(e, t("playlists.pushFailed"));
    } finally {
      setPushing(false);
    }
  }

  async function syncYoutube() {
    if (syncing) return;
    setSyncing(true);
    try {
      const r = await api.syncYoutubePlaylists();
      qc.invalidateQueries({ queryKey: ["playlists"] });
      qc.invalidateQueries({ queryKey: ["playlist"] });
      notify({ level: "success", message: t("playlists.syncedToast", { count: r.synced }) });
    } catch (e) {
      notifyYouTubeActionError(e, t("playlists.syncFailed"));
    } finally {
      setSyncing(false);
    }
  }

  const createMut = useMutation({
    mutationFn: (name: string) => api.createPlaylist(name),
    onSuccess: (pl: Playlist) => {
      setNewName("");
      qc.invalidateQueries({ queryKey: ["playlists"] });
      setSelectedId(pl.id);
    },
  });

  function refreshAll() {
    qc.invalidateQueries({ queryKey: ["playlists"] });
    qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
  }

  function onDragEnd(e: DragEndEvent) {
    const { active: a, over } = e;
    if (!over || a.id === over.id || selectedId == null) return;
    const oldIndex = items.findIndex((v) => v.id === a.id);
    const newIndex = items.findIndex((v) => v.id === over.id);
    if (oldIndex < 0 || newIndex < 0) return;
    // A manual drag breaks any active sort/grouping; reflect that in the controls.
    setSortKey("manual");
    setGroupBy(false);
    order.set(arrayMove(items, oldIndex, newIndex)); // onApply persists
  }

  async function removeItem(videoId: string) {
    if (selectedId == null) return;
    // Membership changes aren't undoable: reset the order to the new set (clearing history)
    // and keep lastSetRef in sync so the follow-up refetch doesn't reset again.
    const next = items.filter((v) => v.id !== videoId);
    order.reset(next);
    lastSetRef.current = idsKey(next);
    try {
      await api.removeFromPlaylist(selectedId, videoId);
      refreshAll();
    } catch {
      // The optimistic removal above didn't stick — resync from the server to restore the row.
      // api.req already surfaced the error to the user via the global error dialog.
      refreshAll();
    }
  }

  async function saveRename() {
    if (selectedId == null) return;
    const name = renameValue.trim();
    setRenaming(false);
    if (name && name !== detail?.name) {
      await api.renamePlaylist(selectedId, name);
      refreshAll();
    }
  }

  async function deletePlaylist() {
    if (selectedId == null || !detail) return;
    const ok = await confirm({
      title: t("playlists.delete"),
      message: t("playlists.confirmDelete", { name: detail.name }),
      confirmLabel: t("playlists.delete"),
      danger: true,
    });
    if (!ok) return;
    // For a YouTube-linked playlist, offer to delete it on YouTube too (Cancel = here only).
    let onYoutube = false;
    if (linked && canWrite) {
      onYoutube = await confirm({
        title: t("playlists.deleteOnYoutubeTitle"),
        message: t("playlists.deleteOnYoutubeMsg", { name: detail.name }),
        confirmLabel: t("playlists.deleteOnYoutubeConfirm"),
        cancelLabel: t("playlists.deleteHereOnly"),
        danger: true,
      });
    }
    const deletedId = selectedId;
    // Pick the next selection from what's left *now* (don't go via null, or the
    // auto-select effect would re-pick the just-deleted id from the stale list).
    const remaining = playlists.filter((p) => p.id !== deletedId);
    setSelectedId(remaining[0]?.id ?? null);
    try {
      await api.deletePlaylist(deletedId, onYoutube);
    } catch {
      notify({ level: "warning", message: t("playlists.deleteYtFailed") });
    }
    qc.removeQueries({ queryKey: ["playlist", deletedId] });
    qc.invalidateQueries({ queryKey: ["playlists"] });
  }

  function playerState(id: string, status: string) {
    api.setState(id, status).then(() => {
      qc.invalidateQueries({ queryKey: ["playlist", selectedId] });
      qc.invalidateQueries({ queryKey: ["feed"] });
    });
  }

  return (
    <div className="flex h-full min-h-0">
      <aside className="w-64 shrink-0 border-r border-border bg-surface/40 overflow-y-auto p-3">
        <div className="flex items-center justify-between mb-2">
          <span className="text-sm font-semibold">{t("playlists.title")}</span>
          <button
            onClick={syncYoutube}
            disabled={syncing}
            title={t("playlists.syncYoutube")}
            aria-label={t("playlists.syncYoutube")}
            className="inline-flex items-center gap-1 text-[11px] text-muted hover:text-accent disabled:opacity-40 transition"
          >
            <RefreshCw className={`w-3.5 h-3.5 ${syncing ? "animate-spin" : ""}`} />
            <Youtube className="w-3.5 h-3.5" />
          </button>
        </div>
        {playlists.length > 1 && (
          <div className="flex items-center gap-1 mb-2">
            <select
              value={plSort.key}
              onChange={(e) =>
                setPlSort({ ...plSort, key: e.target.value as PlSort["key"] })
              }
              aria-label={t("playlists.sortLabel")}
              className="flex-1 min-w-0 bg-card border border-border rounded-md px-1.5 py-1 text-[11px] outline-none focus:border-accent"
            >
              <option value="custom">{t("playlists.railSortCustom")}</option>
              <option value="name">{t("playlists.railSortName")}</option>
              <option value="count">{t("playlists.railSortCount")}</option>
              <option value="duration">{t("playlists.railSortDuration")}</option>
            </select>
            <button
              onClick={() =>
                setPlSort({ ...plSort, dir: plSort.dir === "asc" ? "desc" : "asc" })
              }
              disabled={plSort.key === "custom"}
              title={plSort.dir === "asc" ? t("playlists.dirAsc") : t("playlists.dirDesc")}
              className="p-1 rounded-md border border-border text-muted enabled:hover:text-fg enabled:hover:border-accent disabled:opacity-30 transition"
            >
              {plSort.dir === "asc" ? (
                <ArrowUp className="w-3.5 h-3.5" />
              ) : (
                <ArrowDown className="w-3.5 h-3.5" />
              )}
            </button>
            <button
              onClick={() => setPlSort({ ...plSort, dirtyFirst: !plSort.dirtyFirst })}
              title={t("playlists.dirtyFirst")}
              aria-pressed={plSort.dirtyFirst}
              className={`p-1 rounded-md border transition ${
                plSort.dirtyFirst
                  ? "border-accent text-accent"
                  : "border-border text-muted hover:text-fg hover:border-accent"
              }`}
            >
              <Pin className="w-3.5 h-3.5" />
            </button>
          </div>
        )}
        <form
          onSubmit={(e) => {
            e.preventDefault();
            if (newName.trim()) createMut.mutate(newName.trim());
          }}
          className="flex items-center gap-1.5 mb-3 pb-3 border-b border-border"
        >
          <Plus className="w-4 h-4 text-muted shrink-0" />
          <input
            value={newName}
            onChange={(e) => setNewName(e.target.value)}
            placeholder={t("playlists.newPlaylist")}
            className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-none focus:border-accent"
          />
        </form>
        {playlists.length === 0 && !listQuery.isLoading && (
          <div className="text-xs text-muted px-1 py-2">{t("playlists.noneYet")}</div>
        )}
        <div className="space-y-1">
          {sortedPlaylists.map((pl) => (
            <button
              key={pl.id}
              ref={pl.id === selectedId ? selectedRef : null}
              onClick={() => setSelectedId(pl.id)}
              className={`w-full flex items-center gap-2.5 p-2 rounded-lg border transition text-left ${
                pl.id === selectedId
                  ? "bg-accent/15 border-accent"
                  : "border-transparent hover:bg-card/60"
              }`}
            >
              <div className="w-[34px] h-[34px] rounded-md bg-card overflow-hidden shrink-0">
                {pl.cover_thumbnail && (
                  <img src={pl.cover_thumbnail} alt="" className="w-full h-full object-cover" />
                )}
              </div>
              <div className="min-w-0">
                <div className="flex items-center gap-1 text-[13px] text-fg">
                  <span className="truncate">{plName(pl)}</span>
                  {(pl.source === "youtube" || pl.yt_playlist_id) && (
                    <Youtube
                      className={`w-3 h-3 shrink-0 ${pl.dirty ? "text-accent" : "text-muted"}`}
                    />
                  )}
                </div>
                <div className="text-[11px] text-muted">
                  {t("playlists.itemCount", { count: pl.item_count })}
                  {pl.total_duration_seconds > 0 &&
                    ` · ${formatDuration(pl.total_duration_seconds)}`}
                </div>
              </div>
            </button>
          ))}
        </div>
      </aside>

      <main className="flex-1 min-w-0 overflow-y-auto p-4">
        {selectedId == null || !detail ? (
          <div className="text-muted text-sm p-4">
            {selectedId == null ? t("playlists.pickOne") : t("playlists.loading")}
          </div>
        ) : (
          <>
            <div className="flex gap-3 items-start mb-4">
              <div className="w-24 h-[54px] rounded-lg bg-card overflow-hidden shrink-0">
                {items[0]?.thumbnail_url && (
                  <img
                    src={items[0].thumbnail_url}
                    alt=""
                    className="w-full h-full object-cover"
                  />
                )}
              </div>
              <div className="min-w-0 flex-1">
                {renaming ? (
                  <input
                    autoFocus
                    value={renameValue}
                    onChange={(e) => setRenameValue(e.target.value)}
                    onBlur={saveRename}
                    onKeyDown={(e) => {
                      if (e.key === "Enter") saveRename();
                      if (e.key === "Escape") setRenaming(false);
                    }}
                    className="text-lg font-semibold bg-card border border-border rounded-md px-2 py-0.5 outline-none focus:border-accent"
                  />
                ) : (
                  <div className="flex items-center gap-2 min-w-0">
                    <h2 className="text-lg font-semibold truncate">{plName(detail)}</h2>
                    {editable && (
                      <button
                        onClick={() => {
                          setRenameValue(detail.name);
                          setRenaming(true);
                        }}
                        title={t("playlists.rename")}
                        aria-label={t("playlists.rename")}
                        className="shrink-0 text-muted hover:text-fg"
                      >
                        <Pencil className="w-3.5 h-3.5" />
                      </button>
                    )}
                    {detail.yt_playlist_id && (
                      <a
                        href={`https://www.youtube.com/playlist?list=${detail.yt_playlist_id}`}
                        target="_blank"
                        rel="noreferrer noopener"
                        title={t("playlists.openOnYoutube")}
                        aria-label={t("playlists.openOnYoutube")}
                        className="shrink-0 text-muted hover:text-accent"
                      >
                        <ExternalLink className="w-3.5 h-3.5" />
                      </a>
                    )}
                  </div>
                )}
                <div className="text-xs text-muted mt-0.5">
                  {t("playlists.itemCount", { count: items.length })}
                </div>
                <div className="flex gap-1.5 mt-3 flex-wrap items-center">
                  <button
                    onClick={() => items.length && setPlayingIndex(0)}
                    disabled={!items.length}
                    className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg bg-accent text-accent-fg disabled:opacity-40 hover:opacity-90 transition"
                  >
                    <Play className="w-3.5 h-3.5 fill-current" /> {t("playlists.playAll")}
                  </button>
                  {editable && canWrite && (
                    <button
                      onClick={pushToYoutube}
                      disabled={pushing}
                      title={linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")}
                      aria-label={linked ? t("playlists.pushToYoutube") : t("playlists.exportToYoutube")}
                      className={`inline-flex items-center justify-center p-2 rounded-lg border transition disabled:opacity-40 ${
                        detail.dirty
                          ? "border-accent text-accent hover:bg-accent/10"
                          : "border-border text-muted hover:text-fg hover:border-accent"
                      }`}
                    >
                      {pushing ? (
                        <RefreshCw className="w-3.5 h-3.5 animate-spin" />
                      ) : linked ? (
                        <Youtube className="w-3.5 h-3.5" />
                      ) : (
                        <Upload className="w-3.5 h-3.5" />
                      )}
                    </button>
                  )}
                  {linked && detail.dirty && (
                    <button
                      onClick={revertToYoutube}
                      disabled={reverting}
                      title={t("playlists.revert")}
                      aria-label={t("playlists.revert")}
                      className="inline-flex items-center justify-center p-2 rounded-lg border border-border text-muted hover:text-fg hover:border-accent transition disabled:opacity-40"
                    >
                      <RotateCcw className={`w-3.5 h-3.5 ${reverting ? "animate-spin" : ""}`} />
                    </button>
                  )}
                  {editable && (
                    <button
                      onClick={deletePlaylist}
                      title={t("playlists.delete")}
                      aria-label={t("playlists.delete")}
                      className="inline-flex items-center justify-center p-2 rounded-lg border border-border text-muted hover:text-red-400 hover:border-red-400/50 transition"
                    >
                      <Trash2 className="w-3.5 h-3.5" />
                    </button>
                  )}
                  {linked && detail.dirty && (
                    <span
                      title={t("playlists.unsyncedHint")}
                      className="inline-flex items-center gap-1.5 text-[11px] text-accent ml-1"
                    >
                      <span className="w-1.5 h-1.5 rounded-full bg-accent" />
                      {t("playlists.unsynced")}
                    </span>
                  )}
                </div>
              </div>
            </div>

            {editable && items.length > 1 && (
              <div className="flex items-center gap-2 mb-3 flex-wrap max-w-3xl">
                <span className="text-xs text-muted">{t("playlists.sortLabel")}</span>
                <select
                  value={sortKey}
                  onChange={(e) => {
                    const k = e.target.value as SortKey;
                    setSortKey(k);
                    applySort(k, sortDir, groupBy);
                  }}
                  className="bg-card border border-border rounded-md px-2 py-1 text-xs outline-none focus:border-accent"
                >
                  <option value="manual">{t("playlists.sortManual")}</option>
                  <option value="title">{t("playlists.sortTitle")}</option>
                  <option value="duration">{t("playlists.sortDuration")}</option>
                  <option value="channel">{t("playlists.sortChannel")}</option>
                </select>
                <button
                  onClick={() => {
                    const d = sortDir === "asc" ? "desc" : "asc";
                    setSortDir(d);
                    if (sortKey !== "manual" || groupBy) applySort(sortKey, d, groupBy);
                  }}
                  disabled={sortKey === "manual" && !groupBy}
                  title={sortDir === "asc" ? t("playlists.dirAsc") : t("playlists.dirDesc")}
                  className="p-1.5 rounded-lg border border-border text-muted enabled:hover:text-fg enabled:hover:border-accent disabled:opacity-30 transition"
                >
                  {sortDir === "asc" ? (
                    <ArrowUp className="w-4 h-4" />
                  ) : (
                    <ArrowDown className="w-4 h-4" />
                  )}
                </button>
                <label className="inline-flex items-center gap-1.5 text-xs text-muted cursor-pointer select-none">
                  <input
                    type="checkbox"
                    checked={groupBy}
                    onChange={(e) => {
                      setGroupBy(e.target.checked);
                      applySort(sortKey, sortDir, e.target.checked);
                    }}
                    className="accent-accent"
                  />
                  {t("playlists.groupByChannel")}
                </label>
                <div className="ml-auto">
                  <UndoToolbar
                    canUndo={order.canUndo}
                    canRedo={order.canRedo}
                    onUndo={undo}
                    onRedo={redo}
                  />
                </div>
              </div>
            )}

            {items.length === 0 ? (
              <div className="text-sm text-muted grid place-items-center text-center py-12">
                <div>
                  <ListPlus className="w-6 h-6 mx-auto mb-2 opacity-60" />
                  {t("playlists.empty")}
                </div>
              </div>
            ) : (
              <DndContext
                sensors={sensors}
                collisionDetection={closestCenter}
                onDragEnd={onDragEnd}
              >
                <SortableContext
                  items={items.map((v) => v.id)}
                  strategy={verticalListSortingStrategy}
                >
                  <div className="space-y-1.5 max-w-3xl">
                    {items.map((v, i) => (
                      <Row
                        key={v.id}
                        video={v}
                        index={i}
                        onPlay={() => setPlayingIndex(i)}
                        onRemove={() => removeItem(v.id)}
                      />
                    ))}
                  </div>
                </SortableContext>
              </DndContext>
            )}
          </>
        )}
      </main>

      {playingIndex != null && items[playingIndex] && (
        <Suspense fallback={null}>
          <PlayerModal
            video={items[playingIndex]}
            queue={items}
            startIndex={playingIndex}
            startAt={null}
            onClose={() => setPlayingIndex(null)}
            onState={playerState}
          />
        </Suspense>
      )}
    </div>
  );
}
