"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { BlurFade } from "@/components/magicui/blur-fade";
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from "@/components/ui/dialog";
import {
  AlertDialog,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import {
  Pencil,
  Trash2,
  Plus,
  ExternalLink,
  Globe,
  Search,
  Sparkles,
  Upload,
  Download,
  GripVertical,
  Loader2,
  X,
  FolderEdit,
  CheckSquare,
  Check,
  Library,
} from "lucide-react";
import { toast } from "sonner";
import {
  DndContext,
  closestCenter,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
  DragEndEvent,
} from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  rectSortingStrategy,
  useSortable,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import {
  hostnameOf,
  faviconFallback,
  normalizeURL,
  exportToJSON,
  downloadFile,
  parseImport,
} from "@/lib/bookmarks";

interface Bookmark {
  id: number;
  title: string;
  url: string;
  description: string;
  icon: string;
  category: string;
  sortOrder: number;
}

const DEFAULT_CATEGORY = "默认";
const MAX_DELAY = 0.5;

function clampDelay(value: number) {
  return Math.min(value, MAX_DELAY);
}

type ConfirmState = {
  open: boolean;
  title: string;
  description?: string;
  confirmText?: string;
  destructive?: boolean;
  onConfirm?: () => void | Promise<void>;
};

export function BookmarkManager() {
  const [bookmarks, setBookmarks] = useState<Bookmark[]>([]);
  const [loading, setLoading] = useState(true);

  const [search, setSearch] = useState("");
  const [activeCategory, setActiveCategory] = useState<string | null>(null);
  const [selected, setSelected] = useState<Set<number>>(new Set());

  const [editorOpen, setEditorOpen] = useState(false);
  const [editing, setEditing] = useState<Bookmark | null>(null);
  const [importOpen, setImportOpen] = useState(false);
  const [moveOpen, setMoveOpen] = useState(false);
  const [renameTarget, setRenameTarget] = useState<string | null>(null);
  const [confirmState, setConfirmState] = useState<ConfirmState>({
    open: false,
    title: "",
  });

  function askConfirm(opts: Omit<ConfirmState, "open">) {
    setConfirmState({ ...opts, open: true });
  }
  function closeConfirm() {
    setConfirmState((s) => ({ ...s, open: false }));
  }

  async function fetchBookmarks() {
    try {
      const res = await fetch("/api/proxy/bookmarks");
      if (!res.ok) throw new Error("failed");
      const data = await res.json();
      setBookmarks(data.bookmarks || []);
    } catch {
      toast.error("加载书签失败");
      setBookmarks([]);
    } finally {
      setLoading(false);
    }
  }

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

  const categories = useMemo(() => {
    const set = new Set<string>();
    for (const b of bookmarks) set.add(b.category || DEFAULT_CATEGORY);
    return Array.from(set).sort((a, b) => a.localeCompare(b, "zh"));
  }, [bookmarks]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    return bookmarks.filter((b) => {
      if (activeCategory && (b.category || DEFAULT_CATEGORY) !== activeCategory) {
        return false;
      }
      if (!q) return true;
      return (
        b.title.toLowerCase().includes(q) ||
        b.url.toLowerCase().includes(q) ||
        (b.description || "").toLowerCase().includes(q) ||
        hostnameOf(b.url).toLowerCase().includes(q)
      );
    });
  }, [bookmarks, search, activeCategory]);

  const grouped = useMemo(() => {
    const map = new Map<string, Bookmark[]>();
    for (const b of filtered) {
      const cat = b.category || DEFAULT_CATEGORY;
      if (!map.has(cat)) map.set(cat, []);
      map.get(cat)!.push(b);
    }
    return Array.from(map.entries()).sort(([a], [b]) =>
      a.localeCompare(b, "zh"),
    );
  }, [filtered]);

  const isFiltering = !!search.trim() || !!activeCategory;

  function toggleSelect(id: number) {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  }

  function selectAllVisible() {
    setSelected(new Set(filtered.map((b) => b.id)));
  }

  function clearSelected() {
    setSelected(new Set());
  }

  function openCreate() {
    setEditing(null);
    setEditorOpen(true);
  }

  function openEdit(bm: Bookmark) {
    setEditing(bm);
    setEditorOpen(true);
  }

  async function handleDelete(id: number) {
    const target = bookmarks.find((b) => b.id === id);
    askConfirm({
      title: "删除书签",
      description: target
        ? `确认删除「${target.title}」？此操作无法撤销。`
        : "确认删除这个书签？",
      confirmText: "删除",
      destructive: true,
      onConfirm: async () => {
        const res = await fetch(`/api/proxy/bookmarks/${id}`, {
          method: "DELETE",
        });
        if (!res.ok) {
          toast.error("删除失败");
          return;
        }
        toast.success("已删除");
        fetchBookmarks();
      },
    });
  }

  async function handleBulkDelete() {
    if (!selected.size) return;
    askConfirm({
      title: "批量删除",
      description: `确认删除选中的 ${selected.size} 个书签？此操作无法撤销。`,
      confirmText: "删除",
      destructive: true,
      onConfirm: async () => {
        const res = await fetch("/api/proxy/bookmarks/bulk-delete", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ ids: Array.from(selected) }),
        });
        if (!res.ok) {
          toast.error("批量删除失败");
          return;
        }
        const data = await res.json();
        toast.success(`已删除 ${data.deleted} 个书签`);
        clearSelected();
        fetchBookmarks();
      },
    });
  }

  async function handleBulkMove(category: string) {
    if (!selected.size) return;
    const res = await fetch("/api/proxy/bookmarks/bulk-move", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ids: Array.from(selected), category }),
    });
    if (!res.ok) {
      toast.error("移动失败");
      return;
    }
    toast.success(`已移动到「${category}」`);
    setMoveOpen(false);
    clearSelected();
    fetchBookmarks();
  }

  async function handleRename(from: string, to: string) {
    const target = to.trim();
    if (!target || target === from) {
      setRenameTarget(null);
      return;
    }
    const res = await fetch("/api/proxy/bookmarks/rename-category", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ from, to: target }),
    });
    if (!res.ok) {
      toast.error("重命名失败");
      return;
    }
    if (activeCategory === from) setActiveCategory(target);
    toast.success("分类已重命名");
    setRenameTarget(null);
    fetchBookmarks();
  }

  async function persistOrder(next: Bookmark[]) {
    const ids = next.map((b) => b.id);
    const res = await fetch("/api/proxy/bookmarks/reorder", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ order: ids }),
    });
    if (!res.ok) {
      toast.error("排序保存失败");
      fetchBookmarks();
    }
  }

  function handleDragEnd(event: DragEndEvent, scope: Bookmark[]) {
    const { active, over } = event;
    if (!over || active.id === over.id) return;
    const oldIndex = scope.findIndex((b) => b.id === active.id);
    const newIndex = scope.findIndex((b) => b.id === over.id);
    if (oldIndex < 0 || newIndex < 0) return;
    const reordered = arrayMove(scope, oldIndex, newIndex);
    const idsByCat = new Set(reordered.map((b) => b.id));
    const next = [
      ...reordered,
      ...bookmarks.filter((b) => !idsByCat.has(b.id)),
    ];
    setBookmarks(next.map((b, i) => ({ ...b, sortOrder: i })));
    persistOrder(next);
  }

  return (
    <div className="space-y-6">
      <BlurFade inView delay={0.05}>
        <header className="flex flex-wrap items-end justify-between gap-3">
          <div className="space-y-1.5">
            <div className="inline-flex items-center gap-2.5">
              <div className="flex size-9 items-center justify-center rounded-xl bg-primary/10 text-primary">
                <Library className="size-5" />
              </div>
              <h1 className="text-2xl font-bold tracking-tight sm:text-3xl">
                书签管理
              </h1>
            </div>
            <p className="text-sm text-muted-foreground">
              添加、整理与导入导出公共导航书签
            </p>
          </div>
          {!loading && bookmarks.length > 0 && (
            <div className="flex flex-wrap gap-2 text-xs">
              <StatPill
                value={bookmarks.length}
                label="书签"
              />
              <StatPill value={categories.length} label="分类" />
              {isFiltering && (
                <StatPill
                  value={filtered.length}
                  label="筛选后"
                  emphasis
                />
              )}
            </div>
          )}
        </header>
      </BlurFade>

      <BlurFade inView delay={0.1}>
        <div className="rounded-2xl border bg-card/60 p-3 backdrop-blur sm:p-4">
          <div className="space-y-3">
            <div className="flex flex-wrap items-center gap-2">
              <div className="relative w-full sm:max-w-sm">
                <Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
                <Input
                  value={search}
                  onChange={(e) => setSearch(e.target.value)}
                  placeholder="搜索标题、链接或域名"
                  className="bg-background/70 pl-9 backdrop-blur"
                  aria-label="搜索书签"
                />
                {search && (
                  <button
                    type="button"
                    onClick={() => setSearch("")}
                    aria-label="清空搜索"
                    className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground hover:bg-muted"
                  >
                    <X className="size-3.5" />
                  </button>
                )}
              </div>
              <div className="ml-auto flex items-center gap-2">
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() => setImportOpen(true)}
                >
                  <Upload className="mr-1 size-4" /> 导入
                </Button>
                <Button
                  variant="outline"
                  size="sm"
                  onClick={() =>
                    downloadFile(
                      `bookmarks-${Date.now()}.json`,
                      exportToJSON(bookmarks),
                    )
                  }
                >
                  <Download className="mr-1 size-4" /> 导出
                </Button>
                <Button size="sm" onClick={openCreate}>
                  <Plus className="mr-1 size-4" /> 添加
                </Button>
              </div>
            </div>
            {categories.length > 0 && (
              <CategoryBar
                categories={categories}
                active={activeCategory}
                counts={categoryCounts(bookmarks)}
                total={bookmarks.length}
                onSelect={setActiveCategory}
                onRename={setRenameTarget}
              />
            )}
          </div>
        </div>
      </BlurFade>

      {selected.size > 0 && (
        <BulkBar
          count={selected.size}
          totalVisible={filtered.length}
          onSelectAll={selectAllVisible}
          onClear={clearSelected}
          onMove={() => setMoveOpen(true)}
          onDelete={handleBulkDelete}
        />
      )}

      {loading ? (
        <BookmarksSkeleton />
      ) : bookmarks.length === 0 ? (
        <BlurFade inView delay={0.2}>
          <EmptyState onAdd={openCreate} onImport={() => setImportOpen(true)} />
        </BlurFade>
      ) : filtered.length === 0 ? (
        <BlurFade inView delay={0.2}>
          <div className="rounded-xl border border-dashed border-border bg-card/50 py-10 text-center text-sm text-muted-foreground backdrop-blur">
            没有匹配的书签
          </div>
        </BlurFade>
      ) : (
        <div className="space-y-6">
          {grouped.map(([cat, items], catIdx) => (
            <BlurFade key={cat} inView delay={clampDelay(0.2 + catIdx * 0.05)}>
              <CategorySection
                category={cat}
                items={items}
                selected={selected}
                draggable={!isFiltering}
                onToggleSelect={toggleSelect}
                onEdit={openEdit}
                onDelete={handleDelete}
                onDragEnd={(ev) => handleDragEnd(ev, items)}
                onRename={() => setRenameTarget(cat)}
              />
            </BlurFade>
          ))}
        </div>
      )}

      <BookmarkEditor
        open={editorOpen}
        onOpenChange={setEditorOpen}
        editing={editing}
        categories={categories}
        onSaved={() => {
          setEditorOpen(false);
          fetchBookmarks();
        }}
      />

      <ImportDialog
        open={importOpen}
        onOpenChange={setImportOpen}
        onImported={() => {
          setImportOpen(false);
          fetchBookmarks();
        }}
      />

      <MoveDialog
        open={moveOpen}
        onOpenChange={setMoveOpen}
        categories={categories}
        onMove={handleBulkMove}
      />

      <RenameDialog
        from={renameTarget}
        onCancel={() => setRenameTarget(null)}
        onSubmit={(to) => renameTarget && handleRename(renameTarget, to)}
      />

      <ConfirmDialog
        state={confirmState}
        onClose={closeConfirm}
      />
    </div>
  );
}

function BookmarksSkeleton() {
  return (
    <div className="space-y-6">
      {[0, 1].map((s) => (
        <div key={s}>
          <Skeleton className="mb-3 h-3 w-24" />
          <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
            {[0, 1, 2].map((c) => (
              <Card key={c}>
                <CardContent className="flex items-start gap-3 p-3">
                  <Skeleton className="size-9 rounded-lg" />
                  <div className="flex-1 space-y-2">
                    <Skeleton className="h-4 w-3/4" />
                    <Skeleton className="h-3 w-1/2" />
                  </div>
                </CardContent>
              </Card>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

function ConfirmDialog({
  state,
  onClose,
}: {
  state: ConfirmState;
  onClose: () => void;
}) {
  const [busy, setBusy] = useState(false);

  async function handleConfirm() {
    if (!state.onConfirm) {
      onClose();
      return;
    }
    setBusy(true);
    try {
      await state.onConfirm();
    } finally {
      setBusy(false);
      onClose();
    }
  }

  return (
    <AlertDialog open={state.open} onOpenChange={(v) => !v && !busy && onClose()}>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>{state.title}</AlertDialogTitle>
          {state.description && (
            <AlertDialogDescription>{state.description}</AlertDialogDescription>
          )}
        </AlertDialogHeader>
        <AlertDialogFooter>
          <Button
            type="button"
            variant="outline"
            size="sm"
            onClick={onClose}
            disabled={busy}
          >
            取消
          </Button>
          <Button
            type="button"
            variant={state.destructive ? "destructive" : "default"}
            size="sm"
            onClick={handleConfirm}
            disabled={busy}
          >
            {busy && <Loader2 className="mr-1 size-4 animate-spin" />}
            {state.confirmText || "确认"}
          </Button>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

function categoryCounts(bms: Bookmark[]) {
  const m = new Map<string, number>();
  for (const b of bms) {
    const cat = b.category || DEFAULT_CATEGORY;
    m.set(cat, (m.get(cat) || 0) + 1);
  }
  return m;
}

function CategoryBar({
  categories,
  active,
  counts,
  total,
  onSelect,
  onRename,
}: {
  categories: string[];
  active: string | null;
  counts: Map<string, number>;
  total: number;
  onSelect: (c: string | null) => void;
  onRename: (c: string) => void;
}) {
  if (categories.length === 0) return null;
  return (
    <div className="flex flex-wrap gap-2">
      <Chip
        label="全部"
        count={total}
        active={active === null}
        onClick={() => onSelect(null)}
      />
      {categories.map((c) => (
        <Chip
          key={c}
          label={c}
          count={counts.get(c) || 0}
          active={active === c}
          onClick={() => onSelect(c)}
          onLongClick={() => onRename(c)}
        />
      ))}
    </div>
  );
}

function Chip({
  label,
  count,
  active,
  onClick,
  onLongClick,
}: {
  label: string;
  count: number;
  active: boolean;
  onClick: () => void;
  onLongClick?: () => void;
}) {
  return (
    <button
      onClick={onClick}
      onContextMenu={(e) => {
        if (!onLongClick) return;
        e.preventDefault();
        onLongClick();
      }}
      className={`group inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors ${
        active
          ? "border-primary bg-primary text-primary-foreground"
          : "border-border bg-card/60 text-foreground/70 hover:bg-muted hover:text-foreground"
      }`}
    >
      <span>{label}</span>
      <span
        className={`rounded-full px-1.5 text-[10px] ${
          active
            ? "bg-primary-foreground/20"
            : "bg-muted text-muted-foreground"
        }`}
      >
        {count}
      </span>
    </button>
  );
}

function StatPill({
  value,
  label,
  emphasis,
}: {
  value: number;
  label: string;
  emphasis?: boolean;
}) {
  return (
    <span
      className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 backdrop-blur ${
        emphasis
          ? "border-primary/30 bg-primary/10 text-primary"
          : "border-border bg-card/60 text-muted-foreground"
      }`}
    >
      <span
        className={`font-semibold tabular-nums ${
          emphasis ? "text-primary" : "text-foreground"
        }`}
      >
        {value}
      </span>
      <span>{label}</span>
    </span>
  );
}

function BulkBar({
  count,
  totalVisible,
  onSelectAll,
  onClear,
  onMove,
  onDelete,
}: {
  count: number;
  totalVisible: number;
  onSelectAll: () => void;
  onClear: () => void;
  onMove: () => void;
  onDelete: () => void;
}) {
  return (
    <div className="sticky top-2 z-10 flex flex-wrap items-center gap-2 rounded-xl border bg-background/95 p-2 shadow-sm backdrop-blur">
      <span className="px-2 text-sm font-medium">已选 {count}</span>
      <Button variant="ghost" size="sm" onClick={onSelectAll}>
        <CheckSquare className="mr-1 size-4" />
        选中可见 ({totalVisible})
      </Button>
      <div className="ml-auto flex gap-2">
        <Button variant="outline" size="sm" onClick={onMove}>
          <FolderEdit className="mr-1 size-4" /> 移动到…
        </Button>
        <Button variant="outline" size="sm" onClick={onDelete}>
          <Trash2 className="mr-1 size-4 text-destructive" /> 删除
        </Button>
        <Button variant="ghost" size="sm" onClick={onClear} aria-label="清空选择">
          <X className="size-4" />
        </Button>
      </div>
    </div>
  );
}

function CategorySection({
  category,
  items,
  selected,
  draggable,
  onToggleSelect,
  onEdit,
  onDelete,
  onDragEnd,
  onRename,
}: {
  category: string;
  items: Bookmark[];
  selected: Set<number>;
  draggable: boolean;
  onToggleSelect: (id: number) => void;
  onEdit: (bm: Bookmark) => void;
  onDelete: (id: number) => void;
  onDragEnd: (e: DragEndEvent) => void;
  onRename: () => void;
}) {
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );

  const ids = items.map((b) => b.id);

  return (
    <div>
      <div className="mb-3 flex items-center justify-between">
        <h2 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
          {category}
          <span className="ml-2 text-muted-foreground/60">{items.length}</span>
        </h2>
        <button
          onClick={onRename}
          className="text-xs text-muted-foreground hover:text-foreground"
        >
          重命名
        </button>
      </div>
      <DndContext
        sensors={sensors}
        collisionDetection={closestCenter}
        onDragEnd={onDragEnd}
      >
        <SortableContext items={ids} strategy={rectSortingStrategy}>
          <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
            {items.map((bm) => (
              <SortableBookmarkCard
                key={bm.id}
                bookmark={bm}
                selected={selected.has(bm.id)}
                draggable={draggable}
                onToggleSelect={() => onToggleSelect(bm.id)}
                onEdit={() => onEdit(bm)}
                onDelete={() => onDelete(bm.id)}
              />
            ))}
          </div>
        </SortableContext>
      </DndContext>
    </div>
  );
}

function SortableBookmarkCard({
  bookmark,
  selected,
  draggable,
  onToggleSelect,
  onEdit,
  onDelete,
}: {
  bookmark: Bookmark;
  selected: boolean;
  draggable: boolean;
  onToggleSelect: () => void;
  onEdit: () => void;
  onDelete: () => void;
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: bookmark.id, disabled: !draggable });

  const style: React.CSSProperties = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
    zIndex: isDragging ? 10 : 0,
  };

  const host = hostnameOf(bookmark.url);

  return (
    <Card
      ref={setNodeRef}
      style={style}
      className={`group relative transition-all ${
        isDragging
          ? ""
          : "hover:-translate-y-0.5 hover:shadow-md focus-within:ring-2 focus-within:ring-ring/40 active:translate-y-0"
      } ${selected ? "ring-2 ring-primary" : ""}`}
    >
      <CardContent className="flex items-start gap-3 p-3">
        <button
          type="button"
          aria-label={selected ? "取消选中" : "选中"}
          aria-pressed={selected}
          onClick={onToggleSelect}
          className={`mt-0.5 flex size-5 shrink-0 items-center justify-center rounded border transition-colors sm:size-4 ${
            selected
              ? "border-primary bg-primary text-primary-foreground"
              : "border-muted-foreground/40 group-hover:opacity-100 sm:opacity-0"
          }`}
        >
          {selected && <Check className="size-3" strokeWidth={3} />}
        </button>

        <BookmarkIcon icon={bookmark.icon} url={bookmark.url} />

        <div className="min-w-0 flex-1">
          <a
            href={bookmark.url}
            target="_blank"
            rel="noopener noreferrer"
            className="flex items-center gap-1 font-medium hover:underline"
          >
            <span className="truncate">{bookmark.title}</span>
            <ExternalLink className="size-3 shrink-0 text-muted-foreground" />
          </a>
          {host && (
            <p className="truncate text-[11px] text-muted-foreground/70">
              {host}
            </p>
          )}
          {bookmark.description && (
            <p className="mt-0.5 line-clamp-2 text-xs text-muted-foreground">
              {bookmark.description}
            </p>
          )}
        </div>

        <div className="flex shrink-0 flex-col gap-1">
          <div className="flex gap-1 transition-opacity sm:opacity-0 sm:group-hover:opacity-100">
            <Button
              variant="ghost"
              size="icon-sm"
              onClick={onEdit}
              aria-label="编辑"
            >
              <Pencil className="size-3.5" />
            </Button>
            <Button
              variant="ghost"
              size="icon-sm"
              onClick={onDelete}
              aria-label="删除"
            >
              <Trash2 className="size-3.5 text-destructive" />
            </Button>
          </div>
          {draggable && (
            <button
              type="button"
              {...attributes}
              {...listeners}
              className="flex size-7 cursor-grab items-center justify-center rounded text-muted-foreground/40 transition-colors hover:bg-muted hover:text-muted-foreground active:cursor-grabbing"
              aria-label="拖动排序"
            >
              <GripVertical className="size-4" />
            </button>
          )}
        </div>
      </CardContent>
    </Card>
  );
}

function BookmarkIcon({ icon, url }: { icon: string; url: string }) {
  const initial = icon || faviconFallback(url);
  const [prevInitial, setPrevInitial] = useState(initial);
  const [src, setSrc] = useState(initial);
  const [failed, setFailed] = useState(false);

  if (prevInitial !== initial) {
    setPrevInitial(initial);
    setSrc(initial);
    setFailed(false);
  }

  if (!src || failed) {
    return (
      <div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted">
        <Globe className="size-4 text-muted-foreground" />
      </div>
    );
  }
  return (
    <div className="flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted">
      <img
        src={src}
        alt=""
        width={20}
        height={20}
        loading="lazy"
        className="size-5 object-contain"
        onError={() => {
          if (icon && src === icon) setSrc(faviconFallback(url));
          else setFailed(true);
        }}
      />
    </div>
  );
}

function EmptyState({
  onAdd,
  onImport,
}: {
  onAdd: () => void;
  onImport: () => void;
}) {
  return (
    <div className="rounded-2xl border border-dashed border-border bg-card/60 py-12 text-center backdrop-blur">
      <Globe className="mx-auto mb-3 size-8 text-muted-foreground" />
      <p className="mb-3 text-muted-foreground">还没有书签</p>
      <div className="flex justify-center gap-2">
        <Button size="sm" onClick={onAdd}>
          <Plus className="mr-1 size-4" /> 添加第一个
        </Button>
        <Button variant="outline" size="sm" onClick={onImport}>
          <Upload className="mr-1 size-4" /> 导入
        </Button>
      </div>
    </div>
  );
}

function BookmarkEditor({
  open,
  onOpenChange,
  editing,
  categories,
  onSaved,
}: {
  open: boolean;
  onOpenChange: (v: boolean) => void;
  editing: Bookmark | null;
  categories: string[];
  onSaved: () => void;
}) {
  const [title, setTitle] = useState("");
  const [url, setUrl] = useState("");
  const [description, setDescription] = useState("");
  const [icon, setIcon] = useState("");
  const [category, setCategory] = useState("");
  const [sortOrder, setSortOrder] = useState(0);
  const [fetching, setFetching] = useState(false);
  const [saving, setSaving] = useState(false);

  useEffect(() => {
    if (!open) return;
    if (editing) {
      setTitle(editing.title);
      setUrl(editing.url);
      setDescription(editing.description);
      setIcon(editing.icon);
      setCategory(editing.category);
      setSortOrder(editing.sortOrder);
    } else {
      setTitle("");
      setUrl("");
      setDescription("");
      setIcon("");
      setCategory("");
      setSortOrder(0);
    }
  }, [open, editing]);

  async function autoFetch() {
    if (!url.trim()) {
      toast.warning("先填写链接");
      return;
    }
    setFetching(true);
    try {
      const res = await fetch("/api/proxy/bookmarks/meta", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ url: normalizeURL(url) }),
      });
      if (!res.ok) {
        const data = await res.json().catch(() => ({}));
        toast.error(data.error ? `抓取失败：${data.error}` : "抓取失败");
        return;
      }
      const data = await res.json();
      if (!title && data.title) setTitle(data.title);
      if (!description && data.description) setDescription(data.description);
      if (!icon && data.icon) setIcon(data.icon);
      toast.success("已抓取页面信息");
    } catch (err) {
      toast.error(`抓取失败：${err instanceof Error ? err.message : "网络错误"}`);
    } finally {
      setFetching(false);
    }
  }

  async function handleSave() {
    if (!title.trim() || !url.trim()) {
      toast.warning("标题和链接必填");
      return;
    }
    setSaving(true);
    try {
      const payload = {
        title: title.trim(),
        url: normalizeURL(url),
        description: description.trim(),
        icon: icon.trim(),
        category: category.trim() || DEFAULT_CATEGORY,
        sortOrder,
      };
      const res = editing
        ? await fetch(`/api/proxy/bookmarks/${editing.id}`, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
          })
        : await fetch("/api/proxy/bookmarks", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
          });
      if (!res.ok) {
        toast.error("保存失败");
        return;
      }
      toast.success(editing ? "已更新" : "已添加");
      onSaved();
    } finally {
      setSaving(false);
    }
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-md">
        <DialogHeader>
          <DialogTitle>{editing ? "编辑书签" : "添加书签"}</DialogTitle>
        </DialogHeader>
        <div className="space-y-3 py-2">
          <div>
            <label className="mb-1 block text-sm font-medium">链接 *</label>
            <div className="flex gap-2">
              <Input
                value={url}
                onChange={(e) => setUrl(e.target.value)}
                placeholder="https://github.com"
              />
              <Button
                type="button"
                variant="outline"
                size="sm"
                onClick={autoFetch}
                disabled={fetching}
              >
                {fetching ? (
                  <Loader2 className="size-4 animate-spin" />
                ) : (
                  <Sparkles className="size-4" />
                )}
                <span className="ml-1 hidden sm:inline">抓取</span>
              </Button>
            </div>
            <p className="mt-1 text-xs text-muted-foreground">
              填好链接后点击抓取，自动补全标题、描述、图标
            </p>
          </div>
          <div>
            <label className="mb-1 block text-sm font-medium">标题 *</label>
            <Input
              value={title}
              onChange={(e) => setTitle(e.target.value)}
              placeholder="GitHub"
            />
          </div>
          <div>
            <label className="mb-1 block text-sm font-medium">描述</label>
            <Input
              value={description}
              onChange={(e) => setDescription(e.target.value)}
              placeholder="简短描述"
            />
          </div>
          <div className="grid grid-cols-2 gap-3">
            <div>
              <label className="mb-1 block text-sm font-medium">分类</label>
              <Input
                value={category}
                onChange={(e) => setCategory(e.target.value)}
                placeholder={DEFAULT_CATEGORY}
                list="bookmark-cats"
              />
              <datalist id="bookmark-cats">
                {categories.map((c) => (
                  <option key={c} value={c} />
                ))}
              </datalist>
            </div>
            <div>
              <label className="mb-1 block text-sm font-medium">排序</label>
              <Input
                type="number"
                value={sortOrder}
                onChange={(e) => setSortOrder(Number(e.target.value))}
              />
            </div>
          </div>
          <div>
            <label className="mb-1 block text-sm font-medium">图标 URL</label>
            <Input
              value={icon}
              onChange={(e) => setIcon(e.target.value)}
              placeholder="留空时自动从域名拉取 favicon"
            />
          </div>
        </div>
        <DialogFooter>
          <Button
            variant="outline"
            size="sm"
            onClick={() => onOpenChange(false)}
            disabled={saving}
          >
            取消
          </Button>
          <Button size="sm" onClick={handleSave} disabled={saving}>
            {saving ? <Loader2 className="mr-1 size-4 animate-spin" /> : null}
            保存
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function ImportDialog({
  open,
  onOpenChange,
  onImported,
}: {
  open: boolean;
  onOpenChange: (v: boolean) => void;
  onImported: () => void;
}) {
  const [text, setText] = useState("");
  const [busy, setBusy] = useState(false);
  const fileRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (!open) setText("");
  }, [open]);

  async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;
    const content = await file.text();
    setText(content);
  }

  async function handleImport() {
    const items = parseImport(text);
    if (items.length === 0) {
      toast.warning("没有可导入的书签，请检查格式");
      return;
    }
    setBusy(true);
    try {
      const res = await fetch("/api/proxy/bookmarks/bulk", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ bookmarks: items }),
      });
      if (!res.ok) {
        toast.error("导入失败");
        return;
      }
      const data = await res.json();
      toast.success(`已导入 ${data.created} 个书签`);
      onImported();
    } finally {
      setBusy(false);
    }
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
        <DialogHeader>
          <DialogTitle>导入书签</DialogTitle>
        </DialogHeader>
        <div className="space-y-3 py-2">
          <p className="text-sm text-muted-foreground">
            支持浏览器导出的 HTML 书签文件，或粘贴 JSON 数组。
          </p>
          <div className="flex items-center gap-2">
            <input
              ref={fileRef}
              type="file"
              accept=".html,.htm,.json,.txt"
              onChange={handleFile}
              className="hidden"
            />
            <Button
              type="button"
              variant="outline"
              size="sm"
              onClick={() => fileRef.current?.click()}
            >
              <Upload className="mr-1 size-4" /> 选择文件
            </Button>
            {text && (
              <span className="text-xs text-muted-foreground">
                {(text.length / 1024).toFixed(1)} KB 已就绪
              </span>
            )}
          </div>
          <textarea
            value={text}
            onChange={(e) => setText(e.target.value)}
            placeholder="或者直接粘贴 HTML / JSON 内容"
            className="h-48 w-full rounded-md border bg-background p-2 text-xs font-mono"
          />
        </div>
        <DialogFooter>
          <Button
            variant="outline"
            size="sm"
            onClick={() => onOpenChange(false)}
            disabled={busy}
          >
            取消
          </Button>
          <Button size="sm" onClick={handleImport} disabled={busy || !text}>
            {busy ? <Loader2 className="mr-1 size-4 animate-spin" /> : null}
            导入
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function MoveDialog({
  open,
  onOpenChange,
  categories,
  onMove,
}: {
  open: boolean;
  onOpenChange: (v: boolean) => void;
  categories: string[];
  onMove: (cat: string) => void;
}) {
  const [value, setValue] = useState("");
  const [prevOpen, setPrevOpen] = useState(open);
  if (prevOpen !== open) {
    setPrevOpen(open);
    if (!open) setValue("");
  }

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader>
          <DialogTitle>移动到分类</DialogTitle>
        </DialogHeader>
        <div className="space-y-3 py-2">
          <div className="flex flex-wrap gap-2">
            {categories.map((c) => (
              <button
                key={c}
                onClick={() => onMove(c)}
                className="rounded-full border px-3 py-1 text-xs hover:bg-muted"
              >
                {c}
              </button>
            ))}
          </div>
          <div className="flex gap-2">
            <Input
              value={value}
              onChange={(e) => setValue(e.target.value)}
              placeholder="或输入新分类名"
              onKeyDown={(e) => e.key === "Enter" && value && onMove(value.trim())}
            />
            <Button
              size="sm"
              onClick={() => value.trim() && onMove(value.trim())}
              disabled={!value.trim()}
            >
              移动
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  );
}

function RenameDialog({
  from,
  onCancel,
  onSubmit,
}: {
  from: string | null;
  onCancel: () => void;
  onSubmit: (to: string) => void;
}) {
  const [value, setValue] = useState(from || "");
  const [prevFrom, setPrevFrom] = useState(from);
  if (prevFrom !== from) {
    setPrevFrom(from);
    setValue(from || "");
  }

  return (
    <Dialog open={!!from} onOpenChange={(v) => !v && onCancel()}>
      <DialogContent className="sm:max-w-sm">
        <DialogHeader>
          <DialogTitle>重命名分类</DialogTitle>
        </DialogHeader>
        <div className="py-2">
          <Input
            value={value}
            onChange={(e) => setValue(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && onSubmit(value)}
            placeholder="新分类名"
          />
        </div>
        <DialogFooter>
          <Button variant="outline" size="sm" onClick={onCancel}>
            取消
          </Button>
          <Button size="sm" onClick={() => onSubmit(value)} disabled={!value.trim()}>
            保存
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}
