import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
import {
  AlignLeft,
  ArrowLeft,
  Book,
  BookOpen,
  Braces,
  Check,
  CheckSquare,
  ChevronLeft,
  ChevronRight,
  ClipboardPaste,
  Code2,
  Copy,
  Download,
  Eye,
  FilePlus,
  Folder,
  FolderPlus,
  HardDrive,
  Hash,
  Home,
  Image as ImageIcon,
  Layers,
  Loader2,
  MoreVertical,
  Move,
  Pencil,
  RefreshCw,
  Search,
  SlidersHorizontal,
  Terminal,
  Trash2,
  Upload,
  X,
} from 'lucide-react';
import { MediaEntry, TEXT_EXTS, DOC_EXTS, CODE_EXTS, MD_EXTS, JSON_EXTS, XML_EXTS, IMAGE_EXTS, CONFIG_EXTS, PLAYLIST_EXTS } from './MediaEntry';
import { mediaSetEntryUrl, type MediaSetEntry } from './MediaSet';
import type { ViewMode } from './MediaViewerEditor';
const MediaViewerEditor = lazy(() => import('./MediaViewerEditor'));

import {
  copyPath,
  createFolder,
  deletePath,
  fileExists,
  getFileContents,
  getWebDAVBaseUrl,
  humanFileSize,
  joinPath,
  listDirectory,
  movePath,
  normalizePath,
  putFileContents,
  splitPath,
  type EntryInfo,
} from '../webdav';
import { toast } from 'sonner';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
} from './ui/dialog';
import { ConfirmDialog, type ConfirmOptions } from './ui/confirm-dialog';

// ─── Types ───────────────────────────────────────────────────────────────────

type SortKey   = 'name' | 'size' | 'date';
type Clipboard = { op: 'copy' | 'move'; paths: string[] };

// Extension sets are imported from MediaEntry.

function defaultViewMode(entry: EntryInfo): ViewMode {
  const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
  if (IMAGE_EXTS.has(ext))  return 'image';
  if (CONFIG_EXTS.has(ext)) return 'config';
  if (MD_EXTS.has(ext))     return 'markdown';
  if (JSON_EXTS.has(ext))   return 'json';
  if (XML_EXTS.has(ext))    return 'xml';
  if (CODE_EXTS.has(ext))   return 'code';
  if (DOC_EXTS.has(ext))    return 'doc';
  if (TEXT_EXTS.has(ext))   return 'text';
  if (PLAYLIST_EXTS.has(ext)) return 'text';
  return 'hex';
}

function availableViewers(entry: EntryInfo): ViewMode[] {
  const def = defaultViewMode(entry);
  const ext = entry.name.split('.').pop()?.toLowerCase() ?? '';
  const map: Record<ViewMode, ViewMode[]> = {
    image:    ext === 'svg' ? ['image', 'xml', 'hex'] : ['image', 'hex'],
    config:   ['config', 'text', 'hex'],
    markdown: ['markdown', 'text', 'hex'],
    json:     ['json', 'text', 'hex'],
    xml:      ['xml', 'text', 'hex'],
    code:     ['code', 'text', 'hex'],
    doc:      ['doc'],
    text:     ['text', 'hex'],
    hex:      ['hex', 'text'],
  };
  return map[def];
}

const VIEWER_LABEL: Record<ViewMode, string> = {
  code: 'Code', text: 'Text', markdown: 'Markdown', json: 'JSON', xml: 'HTML/XML', hex: 'Hex', image: 'Image', config: 'Config', doc: 'Document',
};

// ─── Viewer helpers ───────────────────────────────────────────────────────────

function ViewerModeIcon({ mode, className }: { mode: ViewMode; className?: string }) {
  const cls = className ?? 'w-4 h-4';
  switch (mode) {
    case 'text':     return <AlignLeft className={cls} />;
    case 'markdown': return <BookOpen  className={cls} />;
    case 'json':     return <Braces    className={cls} />;
    case 'code':     return <Terminal  className={cls} />;
    case 'xml':      return <Code2     className={cls} />;
    case 'hex':      return <Hash      className={cls} />;
    case 'image':    return <ImageIcon        className={cls} />;
    case 'config':   return <SlidersHorizontal className={cls} />;
    case 'doc':      return <Book      className={cls} />;
  }
}

// EntryIcon is imported from MediaEntry.

// ─── MarqueeText ──────────────────────────────────────────────────────────────
// Scrolls text left→right when it overflows its container, then back — ping-pong.
// Injects a unique per-instance @keyframes rule with the exact pixel offset so
// no CSS variables are needed (more reliable inside Radix portals).

let _mmSeq = 0;

function MarqueeText({ children, className }: { children?: string; className?: string }) {
  const wrapRef = useRef<HTMLSpanElement>(null);
  const idRef   = useRef(`mm${++_mmSeq}`);
  const [animCSS, setAnimCSS] = useState('');

  useEffect(() => {
    let cancelled = false;
    const id   = idRef.current;
    const wrap = wrapRef.current;
    if (!wrap) return;

    const measure = () => {
      if (cancelled) return;
      const ov = wrap.scrollWidth - wrap.clientWidth;
      if (ov <= 0) { setAnimCSS(''); return; }

      // Inject / update the keyframe with the exact measured pixel offset.
      let el = document.getElementById(`kf-${id}`) as HTMLStyleElement | null;
      if (!el) {
        el = document.createElement('style');
        el.id = `kf-${id}`;
        document.head.appendChild(el);
      }
      el.textContent = `@keyframes ${id}{0%,12%{transform:translateX(0)}88%,100%{transform:translateX(-${ov}px)}}`;

      const secs = (Math.max(2000, ov * 20) / 1000).toFixed(2);
      setAnimCSS(`${id} ${secs}s ease-in-out 0.8s infinite alternate`);
    };

    // Two nested RAFs ensure the portal element has a stable layout before we
    // measure (one RAF fires before browser layout; two guarantees post-layout).
    const r1 = requestAnimationFrame(() => {
      const r2 = requestAnimationFrame(measure);
      return () => cancelAnimationFrame(r2);
    });

    const ro = new ResizeObserver(measure);
    ro.observe(wrap);

    return () => {
      cancelled = true;
      cancelAnimationFrame(r1);
      ro.disconnect();
      document.getElementById(`kf-${id}`)?.remove();
      setAnimCSS('');
    };
  }, [children]);

  return (
    // overflow:hidden clips the scrolling text; minWidth:0 lets the element
    // shrink inside grid/flex parents without forcing them to expand to the
    // full text width (white-space:nowrap lives only on the inner span).
    <span
      ref={wrapRef}
      style={{ overflow: 'hidden', display: 'block', minWidth: 0 }}
      className={className}
    >
      <span style={{ display: 'inline-block', whiteSpace: 'nowrap', animation: animCSS || undefined }}>
        {children}
      </span>
    </span>
  );
}

// ─── ActionsModal ─────────────────────────────────────────────────────────────

interface FolderManagementActions {
  onMountFolder: () => void;
  onConfigureFolder: () => void;
  onNewFolder: () => void;
  onNewFile: () => void;
  onUpload: () => void;
  clipboard: Clipboard | null;
  onPaste: () => void;
  selectedCount: number;
  totalCount: number;
  onSelectAll: () => void;
  isRoot: boolean;
}

interface ActionsModalProps {
  entry: EntryInfo | null;
  onClose: () => void;
  onOpen: (entry: EntryInfo, mode?: ViewMode) => void;
  onMount: (entry: EntryInfo) => void;
  onDownload: (entry: EntryInfo) => void;
  onDuplicate: (entry: EntryInfo) => void;
  onRename: (entry: EntryInfo) => void;
  onCopy: (entry: EntryInfo) => void;
  onCut: (entry: EntryInfo) => void;
  onDelete: (entry: EntryInfo) => void;
  folderManagement?: FolderManagementActions;
}

function ActionsModal({ entry, onClose, onOpen, onMount, onDownload, onDuplicate, onRename, onCopy, onCut, onDelete, folderManagement }: ActionsModalProps) {
  const isFolder = entry?.type === 'folder';
  const fm = folderManagement;

  return (
    <Dialog open={entry !== null} onOpenChange={open => !open && onClose()}>
      <DialogContent className="max-w-sm">
        <DialogHeader className="overflow-hidden min-w-0">
          {/* Keep DialogTitle for accessibility (aria-labelledby); styled text lives in the p below */}
          <DialogTitle className="sr-only">{entry?.name || '/'}</DialogTitle>
          <p className="text-lg font-semibold leading-none pr-6 overflow-hidden min-w-0">
            <MarqueeText>{entry?.name || '/'}</MarqueeText>
          </p>
          <DialogDescription>
            <MarqueeText>{isFolder ? 'Folder' : humanFileSize(entry?.size ?? 0)}</MarqueeText>
          </DialogDescription>
        </DialogHeader>
        {entry && (
          <div className="flex flex-col gap-2">

            {/* Folder management items — current-folder context (header Actions) */}
            {fm && (
              <>
                <button onClick={() => { onClose(); fm.onMountFolder(); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3">
                  <HardDrive className="w-4 h-4 text-amber-600" /> <span>Mount Folder</span>
                </button>
                <button onClick={() => { onClose(); fm.onConfigureFolder(); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3">
                  <SlidersHorizontal className="w-4 h-4 text-violet-600" /> <span>Configure Folder</span>
                </button>
                <div className="border-t border-neutral-100" />
                <button onClick={() => { onClose(); fm.onNewFolder(); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <FolderPlus className="w-4 h-4 text-neutral-500" /> <span>New Folder</span>
                </button>
                <button onClick={() => { onClose(); fm.onNewFile(); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <FilePlus className="w-4 h-4 text-neutral-500" /> <span>New File</span>
                </button>
                <button onClick={() => { onClose(); fm.onUpload(); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <Upload className="w-4 h-4 text-neutral-500" /> <span>Upload Files</span>
                </button>
                {fm.clipboard && (
                  <button onClick={() => { onClose(); fm.onPaste(); }}
                    className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                    <ClipboardPaste className="w-4 h-4 text-neutral-500" />
                    <span>{fm.clipboard.op === 'copy' ? 'Copy' : 'Move'} {fm.clipboard.paths.length} item{fm.clipboard.paths.length !== 1 ? 's' : ''} here</span>
                  </button>
                )}
                <button onClick={() => { onClose(); fm.onSelectAll(); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <CheckSquare className="w-4 h-4 text-neutral-500" />
                  <span>{fm.selectedCount === fm.totalCount && fm.totalCount > 0 ? 'Deselect All' : 'Select All'}</span>
                </button>
                {!fm.isRoot && <div className="border-t border-neutral-100" />}
              </>
            )}

            {/* Open folder — list item only (not current-folder context) */}
            {isFolder && !fm && (
              <button onClick={() => { onClose(); onOpen(entry); }}
                className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3">
                <Folder className="w-4 h-4 text-blue-600" /> <span>Open folder</span>
              </button>
            )}

            {/* File actions */}
            {!isFolder && (
              <>
                <button onClick={() => { onClose(); onMount(entry); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 inline-flex items-center gap-3">
                  <HardDrive className="w-4 h-4 text-amber-600" /> <span>Mount on virtual drive</span>
                </button>
                <button onClick={() => { onClose(); onOpen(entry); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <Eye className="w-4 h-4 text-blue-600" />
                  <span className="flex-1">Open / View</span>
                  <span className="text-xs text-neutral-400">{VIEWER_LABEL[defaultViewMode(entry)]}</span>
                </button>
                {/* {availableViewers(entry).filter(m => m !== defaultViewMode(entry)).map(mode => (
                  <button key={mode} onClick={() => { onClose(); onOpen(entry, mode); }}
                    className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                    <ViewerModeIcon mode={mode} className="w-4 h-4 text-neutral-500" />
                    <span>Open as {VIEWER_LABEL[mode]}</span>
                  </button>
                ))} */}
                <button onClick={() => { onClose(); onDownload(entry); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <Download className="w-4 h-4" /> <span>Download</span>
                </button>
                <button onClick={() => { onClose(); onDuplicate(entry); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <Copy className="w-4 h-4" /> <span>Duplicate</span>
                </button>
              </>
            )}

            {/* Rename / Copy / Move / Delete */}
            {(!fm || !fm.isRoot) && (
              <>
                <div className="border-t border-neutral-100" />
                <button onClick={() => { onClose(); onRename(entry); }}
                  className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                  <Pencil className="w-4 h-4" /> <span>Rename</span>
                </button>
                {!fm && (
                  <>
                    <button onClick={() => { onClose(); onCopy(entry); }}
                      className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                      <Copy className="w-4 h-4" /> <span>Copy</span>
                    </button>
                    <button onClick={() => { onClose(); onCut(entry); }}
                      className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-neutral-50 inline-flex items-center gap-3">
                      <Move className="w-4 h-4" /> <span>Move (Cut)</span>
                    </button>
                  </>
                )}
                <button onClick={() => { onClose(); onDelete(entry); }}
                  className="w-full text-left px-4 py-3 rounded border border-red-200 hover:bg-red-50 text-red-700 inline-flex items-center gap-3">
                  <Trash2 className="w-4 h-4" /> <span>Delete</span>
                </button>
              </>
            )}
          </div>
        )}
      </DialogContent>
    </Dialog>
  );
}

// ─── Props ────────────────────────────────────────────────────────────────────

interface MediaManagerProps {
  initialPath?: string;
  rootPath?: string;
  title?: string;
  config?: any;
  setConfig?: (c: any) => void;
  onBack?: () => void;
  onNavigateToDevice?: (deviceId: string) => void;
}

// ─── File cache (session + localStorage) ─────────────────────────────────────

const _sessionCache = new Map<string, Uint8Array>();
const FM_LS_MAX     = 2 * 1024 * 1024; // only persist ≤ 2 MB to localStorage

function _cacheKey(path: string, size: number, mtime: string | null) {
  return `${path}|${size}|${mtime ?? ''}`;
}

function _lsGet(key: string): Uint8Array | null {
  try {
    const raw = localStorage.getItem('fmcache:' + key);
    if (!raw) return null;
    const bin = atob(raw);
    const out = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
    return out;
  } catch { return null; }
}

function _lsSet(key: string, data: Uint8Array) {
  if (data.length > FM_LS_MAX) return;
  try {
    let b64 = '';
    for (let i = 0; i < data.length; i += 8192) {
      b64 += btoa(
        String.fromCharCode.apply(null, data.slice(i, Math.min(i + 8192, data.length)) as unknown as number[]),
      );
    }
    localStorage.setItem('fmcache:' + key, b64);
  } catch { /* quota exceeded — skip */ }
}

async function _getEntryBytes(entry: EntryInfo): Promise<Uint8Array> {
  const key = _cacheKey(entry.path, entry.size, entry.lastModified?.toISOString() ?? null);
  const sess = _sessionCache.get(key);
  if (sess) return sess;
  const ls = _lsGet(key);
  if (ls) { _sessionCache.set(key, ls); return ls; }
  const blob = await getFileContents(entry.path);
  const data = new Uint8Array(await blob.arrayBuffer());
  _sessionCache.set(key, data);
  _lsSet(key, data);
  return data;
}

// ─── Main component ───────────────────────────────────────────────────────────

const FM_PATH_KEY = 'fileManager.path';

export default function MediaManager({ initialPath, rootPath, title, config, setConfig, onBack, onNavigateToDevice }: MediaManagerProps) {
  const pathKey = rootPath ? `fileManager.path:${rootPath}` : FM_PATH_KEY;
  const [path, setPath] = useState(() => normalizePath(initialPath ?? localStorage.getItem(pathKey) ?? rootPath ?? '/'));
  const [entries, setEntries] = useState<EntryInfo[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError]     = useState<string | null>(null);
  const [selected, setSelected] = useState<Set<string>>(new Set());
  const [filter, setFilter]   = useState(() => localStorage.getItem('fileManager.filter') ?? '');
  const [sortKey, setSortKey] = useState<SortKey>(() => (localStorage.getItem('fileManager.sortKey') as SortKey) ?? 'name');
  const [sortAsc, setSortAsc] = useState(() => localStorage.getItem('fileManager.sortAsc') !== 'false');
  const [clipboard, setClipboard] = useState<Clipboard | null>(null);
  const [actionEntry, setActionEntry] = useState<EntryInfo | null>(null);
  const [folderActionOpen, setFolderActionOpen] = useState(false);
  const [dragOver, setDragOver] = useState(false);
  const [showNewFile, setShowNewFile] = useState(false);
  const [newFileName, setNewFileName] = useState('');

  // Viewer
  const [viewEntry, setViewEntry]   = useState<EntryInfo | null>(null);
  const [viewMode, setViewMode]     = useState<ViewMode | null>(null);
  const [viewText, setViewText]     = useState<string | null>(null);
  const [viewImgUrl, setViewImgUrl] = useState<string | null>(null);
  const [viewHexData, setViewHexData] = useState<Uint8Array | null>(null);
  const [viewLoading, setViewLoading] = useState(false);

  // Rename / folder / VMS
  const [showNewFolder, setShowNewFolder] = useState(false);
  const [newFolderName, setNewFolderName] = useState('');
  const [showCreateVms, setShowCreateVms] = useState(false);
  const [vmsName, setVmsName]             = useState('');
  const [renameEntry, setRenameEntry]   = useState<EntryInfo | null>(null);
  const [renameName, setRenameName]     = useState('');
  const [mountEntry, setMountEntry]     = useState<EntryInfo | null>(null);

  const fileInputRef    = useRef<HTMLInputElement>(null);
  const renameInputRef  = useRef<HTMLInputElement>(null);
  const breadcrumbRef   = useRef<HTMLDivElement>(null);
  const dragCounter     = useRef(0);
  const [confirm, setConfirm] = useState<ConfirmOptions | null>(null);

  // ── Directory loading ────────────────────────────────────────────────────

  const load = useCallback(async (p: string) => {
    setLoading(true);
    setError(null);
    setSelected(new Set());
    try {
      setEntries(await listDirectory(p));
    } catch (e: any) {
      setError(e?.message ?? 'Failed to load directory');
      setEntries([]);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { void load(path); }, [path, load]);
  useEffect(() => {
    const el = breadcrumbRef.current;
    if (el) el.scrollLeft = el.scrollWidth;
  }, [path]);
  useEffect(() => { localStorage.setItem('fileManager.filter',  filter);        }, [filter]);
  useEffect(() => { localStorage.setItem('fileManager.sortKey', sortKey);       }, [sortKey]);
  useEffect(() => { localStorage.setItem('fileManager.sortAsc', String(sortAsc)); }, [sortAsc]);

  // ── Folder config (.config) ──────────────────────────────────────────────

  const [folderConfig, setFolderConfig] = useState<Record<string, string> | null>(null);

  useEffect(() => {
    let cancelled = false;
    setFolderConfig(null);
    getFileContents(joinPath(path, '.config'))
      .then(async blob => {
        if (cancelled) return;
        const cfg: Record<string, string> = {};
        for (const line of (await blob.text()).split('\n')) {
          const t = line.trim();
          if (!t || t.startsWith('#')) continue;
          const eq = t.indexOf('=');
          if (eq < 0) continue;
          cfg[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
        }
        setFolderConfig(cfg);
      })
      .catch(() => { if (!cancelled) setFolderConfig(null); });
    return () => { cancelled = true; };
  }, [path]);

  const navigateTo = (p: string) => {
    let norm = normalizePath(p);
    if (rootPath && !norm.startsWith(rootPath)) norm = rootPath;
    localStorage.setItem(pathKey, norm);
    setPath(norm);
    setFilter('');
  };
  const navigateUp = () => { if (path !== (rootPath ?? '/')) navigateTo(splitPath(path).parent); };

  // ── Sort + filter ────────────────────────────────────────────────────────

  const visible = entries
    .filter(e => !filter || e.name.toLowerCase().includes(filter.toLowerCase()))
    .sort((a, b) => {
      if (a.type !== b.type) return a.type === 'folder' ? -1 : 1;
      let cmp = 0;
      if      (sortKey === 'name') cmp = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
      else if (sortKey === 'size') cmp = a.size - b.size;
      else                         cmp = (a.lastModified?.getTime() ?? 0) - (b.lastModified?.getTime() ?? 0);
      return sortAsc ? cmp : -cmp;
    });

  const toggleSort = (key: SortKey) => {
    if (sortKey === key) setSortAsc(v => !v);
    else { setSortKey(key); setSortAsc(true); }
  };

  // ── Multi-select ─────────────────────────────────────────────────────────

  const toggleSelect = (p: string) =>
    setSelected(prev => { const next = new Set(prev); next.has(p) ? next.delete(p) : next.add(p); return next; });

  const selectAll = () =>
    setSelected(selected.size === visible.length && visible.length > 0
      ? new Set()
      : new Set(visible.map(e => e.path)));

  // ── File viewer ──────────────────────────────────────────────────────────

  const openEntry = async (entry: EntryInfo, mode?: ViewMode) => {
    if (renameEntry !== null) return;
    if (entry.type === 'folder') { navigateTo(joinPath(path, entry.name)); return; }
    const targetMode = mode ?? defaultViewMode(entry);
    if (targetMode === 'doc') { window.open(getWebDAVBaseUrl() + entry.path, '_blank'); return; }
    setViewEntry(entry);
    setViewMode(targetMode);
    setViewText(null);
    setViewImgUrl(prev => { if (prev) URL.revokeObjectURL(prev); return null; });
    setViewHexData(null);
    setViewLoading(true);
    try {
      const bytes = await _getEntryBytes(entry);
      if (targetMode === 'image') {
        const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
        const imgExt = entry.name.split('.').pop()?.toLowerCase();
        const imgMime = imgExt === 'svg' ? 'image/svg+xml' : undefined;
        setViewImgUrl(URL.createObjectURL(new Blob([ab], imgMime ? { type: imgMime } : undefined)));
      } else if (targetMode === 'hex') {
        setViewHexData(bytes);
      } else {
        setViewText(new TextDecoder().decode(bytes));
      }
    } catch (e: any) {
      toast.error(`Failed to open ${entry.name}: ${e?.message ?? e}`);
      setViewEntry(null);
      setViewMode(null);
    } finally {
      setViewLoading(false);
    }
  };

  const switchViewMode = async (mode: ViewMode) => {
    if (!viewEntry) return;
    setViewMode(mode);
    setViewLoading(true);
    try {
      const bytes = await _getEntryBytes(viewEntry);
      if (mode === 'image') {
        const ab = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
        const ext = viewEntry.name.split('.').pop()?.toLowerCase();
        const mime = ext === 'svg' ? 'image/svg+xml' : undefined;
        setViewImgUrl(prev => { if (prev) URL.revokeObjectURL(prev); return URL.createObjectURL(new Blob([ab], mime ? { type: mime } : undefined)); });
      } else if (mode === 'hex') {
        setViewHexData(bytes);
      } else {
        setViewText(new TextDecoder().decode(bytes));
      }
    } finally {
      setViewLoading(false);
    }
  };

  const closeViewer = () => {
    setViewImgUrl(prev => { if (prev) URL.revokeObjectURL(prev); return null; });
    setViewEntry(null); setViewMode(null);
    setViewText(null);  setViewHexData(null);
  };

  const saveViewFile = async (content: string | Uint8Array) => {
    if (!viewEntry) throw new Error('No file open');
    const bytes: Uint8Array = typeof content === 'string'
      ? new TextEncoder().encode(content)
      : content;
    await putFileContents(viewEntry.path, bytes);
    if (typeof content === 'string') setViewText(content);
    else setViewHexData(bytes);
    // Keep cache coherent within this session
    const key = _cacheKey(viewEntry.path, viewEntry.size, viewEntry.lastModified?.toISOString() ?? null);
    _sessionCache.set(key, bytes);
    _lsSet(key, bytes);
    // If this is the folder .config, update folderConfig immediately so base_url etc. take effect
    if (viewEntry.name === '.config' && typeof content === 'string') {
      const cfg: Record<string, string> = {};
      for (const line of content.split('\n')) {
        const t = line.trim();
        if (!t || t.startsWith('#')) continue;
        const eq = t.indexOf('=');
        if (eq >= 0) cfg[t.slice(0, eq).trim()] = t.slice(eq + 1).trim();
      }
      setFolderConfig(Object.keys(cfg).length ? cfg : null);
      closeViewer();
    }
    toast.success(`Saved ${viewEntry.name}`);
    void load(path);
  };

  // ── Download ─────────────────────────────────────────────────────────────

  const triggerDownload = (blob: Blob, name: string) => {
    const url = URL.createObjectURL(blob);
    const a   = document.createElement('a');
    a.href = url; a.download = name; a.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  };

  const downloadEntry = async (entry: EntryInfo) => {
    try { triggerDownload(await getFileContents(entry.path), entry.name); }
    catch (e: any) { toast.error(`Download failed: ${e?.message ?? e}`); }
  };

  const downloadSelected = async () => {
    for (const e of entries.filter(e => selected.has(e.path) && e.type === 'file'))
      await downloadEntry(e);
  };

  // ── Delete ───────────────────────────────────────────────────────────────

  const deleteEntry = (entry: EntryInfo) => {
    setConfirm({
      title: entry.type === 'folder' ? `Delete folder "${entry.name}"?` : `Delete "${entry.name}"?`,
      description: entry.type === 'folder' ? 'This will delete the folder and all its contents.' : undefined,
      confirmLabel: 'Delete',
      destructive: true,
      onConfirm: async () => {
        try { await deletePath(entry.path); toast.success(`Deleted ${entry.name}`); void load(path); }
        catch (e: any) { toast.error(`Delete failed: ${e?.message ?? e}`); }
      },
    });
  };

  const deleteSelected = () => {
    const targets = entries.filter(e => selected.has(e.path));
    setConfirm({
      title: `Delete ${targets.length} item${targets.length !== 1 ? 's' : ''}?`,
      confirmLabel: 'Delete',
      destructive: true,
      onConfirm: async () => {
        let failed = 0;
        for (const e of targets) { try { await deletePath(e.path); } catch { failed++; } }
        if (failed) toast.error(`${failed} item${failed !== 1 ? 's' : ''} failed to delete`);
        else        toast.success(`Deleted ${targets.length} item${targets.length !== 1 ? 's' : ''}`);
        void load(path);
        setSelected(new Set());
      },
    });
  };

  // ── Rename ───────────────────────────────────────────────────────────────

  const startRename = (entry: EntryInfo) => {
    setRenameEntry(entry);
    setRenameName(entry.name);
    setActionEntry(null);
    setFolderActionOpen(false);
    setTimeout(() => renameInputRef.current?.focus(), 50);
  };

  const commitRename = async () => {
    if (!renameEntry) return;
    const newName = renameName.trim();
    if (!newName || newName === renameEntry.name) { setRenameEntry(null); return; }
    const dest = joinPath(splitPath(renameEntry.path).parent, newName);
    try {
      await movePath(renameEntry.path, dest);
      toast.success(`Renamed to "${newName}"`);
      if (renameEntry.path === path) navigateTo(dest);
      else void load(path);
    } catch (e: any) { toast.error(`Rename failed: ${e?.message ?? e}`); }
    finally { setRenameEntry(null); }
  };

  // ── Mount ────────────────────────────────────────────────────────────────

  const mountOnDevice = async (deviceType: 'drive' | 'meatloaf', key: string) => {
    if (!mountEntry || !setConfig || !config) return;
    const newConfig = JSON.parse(JSON.stringify(config));
    if (!newConfig.devices) newConfig.devices = {};
    if (!newConfig.devices.iec) newConfig.devices.iec = {};
    if (!newConfig.devices.iec[key]) newConfig.devices.iec[key] = { type: deviceType };
    const dev = newConfig.devices.iec[key];

    const mountExt = mountEntry.name.split('.').pop()?.toLowerCase() ?? '';
    if (PLAYLIST_EXTS.has(mountExt)) {
      try {
        const text  = await (await getFileContents(mountEntry.path)).text();
        const dir   = splitPath(mountEntry.path).parent;
        const isVms = mountExt === 'vms';
        const candidates: MediaSetEntry[] = text.split('\n')
          .map(l => l.trim())
          .filter(l => l.length > 0 && !l.startsWith('#'))
          .map(l => {
            if (isVms) {
              const comma = l.indexOf(',');
              if (comma > 0) {
                const url  = l.slice(0, comma).trim();
                const name = l.slice(comma + 1).trim();
                const resolved = url.startsWith('/') ? url : joinPath(dir, url);
                return name ? { url: resolved, name } : resolved;
              }
            }
            return l.startsWith('/') ? l : joinPath(dir, l);
          });
        if (candidates.length === 0) {
          toast.error(`${mountEntry.name}: swap list is empty`);
          return;
        }
        const exists = await Promise.all(candidates.map(e => fileExists(mediaSetEntryUrl(e)).catch(() => false)));
        const files  = candidates.filter((_, i) => exists[i]);
        if (files.length === 0) {
          toast.error(`${mountEntry.name}: no files in swap list exist on device`);
          return;
        }
        if (files.length < 2) {
          toast.error(`${mountEntry.name}: a media set needs more than one item`);
          return;
        }
        if (files.length < candidates.length) {
          toast.warning(`${candidates.length - files.length} missing file(s) skipped from swap list`);
        }
        dev.url       = mediaSetEntryUrl(files[0]);
        dev.media_set = files;
      } catch (e: any) {
        toast.error(`Failed to read ${mountEntry.name}: ${e?.message ?? e}`);
        return;
      }
    } else {
      dev.url = mountEntry.path;
      delete dev.media_set;
    }

    if (!dev.enabled) dev.enabled = 1;
    if (folderConfig?.['base_url']) {
      const resolvedBase = (folderConfig['base_url'] === '.' ? path : folderConfig['base_url']).replace(/\/$/, '');
      if (mountEntry.path.startsWith(resolvedBase + '/') || mountEntry.path === resolvedBase) {
        dev.base_url = resolvedBase;
        dev.url = mountEntry.path.slice(resolvedBase.length) || '/';
      } else {
        delete dev.base_url;
        // dev.url already set to mountEntry.path above
      }
    }
    if (folderConfig?.['cache'] === '.') dev.cache = path;
    setConfig(newConfig);
    setMountEntry(null);
    const deviceId = `${deviceType}-${key}`;
    const isPlaylist = PLAYLIST_EXTS.has(mountExt);
    const label      = isPlaylist
      ? `Loaded "${mountEntry.name}" (${(dev.media_set as MediaSetEntry[]).length} disks) on ${deviceType} #${key}`
      : `Mounted "${mountEntry.name}" on ${deviceType} #${key}`;
    toast.success(label, {
      action: onNavigateToDevice
        ? { label: 'View Device', onClick: () => onNavigateToDevice(deviceId) }
        : undefined,
    });
  };

  // ── Clipboard ────────────────────────────────────────────────────────────

  const cutOrCopyEntry = (entry: EntryInfo, op: 'copy' | 'move') => {
    setClipboard({ op, paths: [entry.path] });
    toast.success(`"${entry.name}" ${op === 'copy' ? 'copied' : 'cut'} — navigate and paste`);
    setActionEntry(null);
  };

  const duplicateEntry = async (entry: EntryInfo) => {
    const { parent } = splitPath(entry.path);
    const dotIdx = entry.name.lastIndexOf('.');
    const base = dotIdx > 0 ? entry.name.slice(0, dotIdx) : entry.name;
    const ext  = dotIdx > 0 ? entry.name.slice(dotIdx) : '';
    let destName = `${base} copy${ext}`;
    let destPath = joinPath(parent, destName);
    let n = 2;
    while (await fileExists(destPath)) {
      destName = `${base} copy ${n}${ext}`;
      destPath = joinPath(parent, destName);
      n++;
    }
    try {
      await copyPath(entry.path, destPath);
      toast.success(`Duplicated as "${destName}"`);
      void load(path);
    } catch (e: any) {
      toast.error(`Duplicate failed: ${e?.message ?? e}`);
    }
  };

  const copySelected = () => {
    setClipboard({ op: 'copy', paths: [...selected] });
    toast.success(`${selected.size} item${selected.size !== 1 ? 's' : ''} copied — navigate and paste`);
    setSelected(new Set());
  };

  const moveSelected = () => {
    setClipboard({ op: 'move', paths: [...selected] });
    toast.success(`${selected.size} item${selected.size !== 1 ? 's' : ''} cut — navigate and paste`);
    setSelected(new Set());
  };

  const paste = async () => {
    if (!clipboard) return;
    let failed = 0;
    for (const src of clipboard.paths) {
      const dest = joinPath(path, splitPath(src).name);
      try { clipboard.op === 'copy' ? await copyPath(src, dest) : await movePath(src, dest); }
      catch { failed++; }
    }
    if (failed) toast.error(`${failed} item${failed !== 1 ? 's' : ''} failed`);
    else        toast.success(`Pasted ${clipboard.paths.length} item${clipboard.paths.length !== 1 ? 's' : ''}`);
    setClipboard(null);
    void load(path);
  };

  // ── Configure folder ─────────────────────────────────────────────────────

  const handleConfigureFolder = async () => {
    const configPath = joinPath(path, '.config');
    try {
      if (!await fileExists(configPath)) {
        await putFileContents(configPath, '');
      }
    } catch { /* open anyway */ }
    const entry: EntryInfo = { name: '.config', path: configPath, type: 'file', size: 0, lastModified: null, contentType: null };
    void openEntry(entry, 'config');
  };

  // ── Create VMS ───────────────────────────────────────────────────────────

  const openCreateVms = () => {
    const folderName = path.split('/').filter(Boolean).pop() ?? 'playlist';
    setVmsName(folderName + '.vms');
    setShowNewFolder(false);
    setShowCreateVms(true);
  };

  const handleCreateVms = async () => {
    const name = vmsName.trim();
    if (!name) return;
    const filename = name.endsWith('.vms') ? name : name + '.vms';
    const selectedFiles = entries.filter(e => selected.has(e.path) && e.type === 'file');
    const content = selectedFiles
      .map(e => {
        const stem = e.name.includes('.') ? e.name.slice(0, e.name.lastIndexOf('.')) : e.name;
        return `${e.name},${stem}`;
      })
      .join('\n') + '\n';
    try {
      await putFileContents(joinPath(path, filename), content);
      toast.success(`Created "${filename}"`);
      setShowCreateVms(false);
      setVmsName('');
      setSelected(new Set());
      void load(path);
    } catch (e: any) { toast.error(`Failed to create VMS: ${e?.message ?? e}`); }
  };

  // ── New folder ───────────────────────────────────────────────────────────

  const handleCreateFolder = async () => {
    const name = newFolderName.trim();
    if (!name) return;
    try {
      await createFolder(joinPath(path, name), true);
      toast.success(`Created folder "${name}"`);
      setShowNewFolder(false); setNewFolderName('');
      navigateTo(joinPath(path, name));
    } catch (e: any) { toast.error(`Failed to create folder: ${e?.message ?? e}`); }
  };

  // ── New file ─────────────────────────────────────────────────────────────

  const handleCreateFile = async () => {
    const name = newFileName.trim();
    if (!name) return;
    try {
      await putFileContents(joinPath(path, name), new Uint8Array(0));
      toast.success(`Created "${name}"`);
      setShowNewFile(false); setNewFileName('');
      void load(path);
    } catch (e: any) { toast.error(`Failed to create file: ${e?.message ?? e}`); }
  };

  // ── Upload ───────────────────────────────────────────────────────────────

  const handleUpload = async (file: File) => {
    const target = joinPath(path, file.name);
    const doUpload = async () => {
      try {
        await putFileContents(target, await file.arrayBuffer());
        toast.success(`Uploaded ${file.name}`);
        void load(path);
      } catch (e: any) { toast.error(`Upload failed for ${file.name}: ${e?.message ?? e}`); }
    };
    if (await fileExists(target)) {
      setConfirm({
        title: `"${file.name}" already exists`,
        description: 'Replace the existing file?',
        confirmLabel: 'Overwrite',
        destructive: true,
        onConfirm: doUpload,
      });
      return;
    }
    await doUpload();
  };

  const onPickFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return;
    Array.from(e.target.files).forEach(f => void handleUpload(f));
    e.target.value = '';
  };

  // ── Drag & drop ──────────────────────────────────────────────────────────

  const onDragEnter = (e: React.DragEvent) => {
    e.preventDefault(); dragCounter.current++;
    if (e.dataTransfer.types.includes('Files')) setDragOver(true);
  };
  const onDragLeave = () => { if (--dragCounter.current <= 0) { dragCounter.current = 0; setDragOver(false); } };
  const onDragOver  = (e: React.DragEvent) => e.preventDefault();
  const onDrop = (e: React.DragEvent) => {
    e.preventDefault(); dragCounter.current = 0; setDragOver(false);
    Array.from(e.dataTransfer.files).forEach(f => void handleUpload(f));
  };

  // ── Derived ──────────────────────────────────────────────────────────────

  const pathParts = path.split('/').filter(Boolean);
  const selCount  = selected.size;


  // ── Render ───────────────────────────────────────────────────────────────

  return (
    <div
      className="relative flex flex-col h-full"
      onDragEnter={onDragEnter}
      onDragLeave={onDragLeave}
      onDragOver={onDragOver}
      onDrop={onDrop}
    >
      {/* ── Header ── */}
      <div className="bg-white border-b border-neutral-200 px-4 py-3 flex-shrink-0">
        <div className="flex items-center gap-2 mb-2">
          {onBack && (
            <button onClick={onBack} className="p-1 rounded hover:bg-neutral-100" title="Back">
              <ChevronLeft className="w-5 h-5" />
            </button>
          )}
          <h2 className="font-semibold flex-1 text-sm">{title ?? 'Media Manager'}</h2>
          <button onClick={() => void load(path)} className="p-1.5 rounded hover:bg-neutral-100" title="Refresh">
            <RefreshCw className="w-4 h-4" />
          </button>
          <button onClick={() => { setShowNewFile(v => !v); setShowNewFolder(false); }} className="p-1.5 rounded hover:bg-neutral-100" title="New File">
            <FilePlus className="w-4 h-4" />
          </button>
          <button onClick={() => { setShowNewFolder(v => !v); setShowNewFile(false); }} className="p-1.5 rounded hover:bg-neutral-100" title="New Folder">
            <FolderPlus className="w-4 h-4" />
          </button>
          <button onClick={() => fileInputRef.current?.click()} className="p-1.5 rounded hover:bg-neutral-100" title="Upload">
            <Upload className="w-4 h-4" />
          </button>
          <button onClick={() => setFolderActionOpen(true)} className="p-1.5 rounded hover:bg-neutral-100" title="Actions">
            <MoreVertical className="w-4 h-4" />
          </button>
          <input ref={fileInputRef} type="file" multiple className="hidden" onChange={onPickFiles} />
        </div>

        {/* Breadcrumb */}
        <div className="flex items-center gap-1 text-sm text-neutral-600">
          <div ref={breadcrumbRef} className="flex items-center gap-1 overflow-x-auto flex-1 min-w-0 [&::-webkit-scrollbar]:hidden" style={{ scrollbarWidth: 'none' }}>
          <button onClick={() => navigateTo(rootPath ?? '/')} className="p-1 rounded hover:bg-neutral-100 flex-shrink-0" title="Root">
            <Home className="w-4 h-4" />
          </button>
          {pathParts.map((part, i) => {
            const isLast    = i === pathParts.length - 1;
            const isEditing = isLast && renameEntry !== null && renameEntry.path === path;
            return (
              <div key={i} className="flex items-center gap-1 flex-shrink-0">
                <ChevronRight className="w-3 h-3 text-neutral-400" />
                {isEditing ? (
                  <input
                    ref={renameInputRef}
                    value={renameName}
                    onChange={e => setRenameName(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === 'Enter') void commitRename();
                      if (e.key === 'Escape') setRenameEntry(null);
                    }}
                    onBlur={() => void commitRename()}
                    onFocus={e => e.currentTarget.select()}
                    className="px-1 py-0 border border-blue-400 rounded text-sm max-w-[120px] min-w-[60px]"
                    autoFocus
                  />
                ) : (
                  <button
                    onClick={() => navigateTo('/' + pathParts.slice(0, i + 1).join('/'))}
                    className="hover:text-blue-600 hover:underline max-w-[120px] truncate"
                  >
                    {part}
                  </button>
                )}
              </div>
            );
          })}
          </div>
          {folderConfig !== null && (
            <button onClick={() => void handleConfigureFolder()} className="p-1 rounded hover:bg-green-100 text-green-500 flex-shrink-0" title="Edit Folder Config">
              <SlidersHorizontal className="w-3.5 h-3.5" strokeWidth={3} />
            </button>
          )}
        </div>
        {(folderConfig?.['base_url']) && (
          <div className="text-xs text-neutral-400 mt-0.5 truncate">
            {folderConfig?.['base_url']}
          </div>
        )}

        {showNewFile && (
          <div className="mt-2 flex items-center gap-2">
            <input
              value={newFileName}
              onChange={e => setNewFileName(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter') void handleCreateFile();
                if (e.key === 'Escape') { setShowNewFile(false); setNewFileName(''); }
              }}
              placeholder="New file name"
              className="flex-1 px-2 py-1 text-sm border border-neutral-300 rounded"
              autoFocus
            />
            <button onClick={() => void handleCreateFile()} className="px-3 py-1 text-sm bg-blue-600 text-white rounded">
              Create
            </button>
          </div>
        )}
        {showNewFolder && (
          <div className="mt-2 flex items-center gap-2">
            <input
              value={newFolderName}
              onChange={e => setNewFolderName(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter') void handleCreateFolder();
                if (e.key === 'Escape') { setShowNewFolder(false); setNewFolderName(''); }
              }}
              placeholder="New folder name"
              className="flex-1 px-2 py-1 text-sm border border-neutral-300 rounded"
              autoFocus
            />
            <button onClick={() => void handleCreateFolder()} className="px-3 py-1 text-sm bg-blue-600 text-white rounded">
              Create
            </button>
          </div>
        )}
        {showCreateVms && (
          <div className="mt-2 flex items-center gap-2">
            <Layers className="w-4 h-4 text-indigo-500 flex-shrink-0" />
            <input
              value={vmsName}
              onChange={e => setVmsName(e.target.value)}
              onKeyDown={e => {
                if (e.key === 'Enter') void handleCreateVms();
                if (e.key === 'Escape') { setShowCreateVms(false); setVmsName(''); }
              }}
              placeholder="playlist.vms"
              className="flex-1 px-2 py-1 text-sm border border-neutral-300 rounded"
              autoFocus
            />
            <button onClick={() => void handleCreateVms()} className="px-3 py-1 text-sm bg-indigo-600 text-white rounded">
              Create
            </button>
            <button onClick={() => { setShowCreateVms(false); setVmsName(''); }} className="p-1 rounded hover:bg-neutral-200">
              <X className="w-4 h-4 text-neutral-500" />
            </button>
          </div>
        )}
      </div>

      {/* ── Filter + sort bar ── */}
      <div className="bg-neutral-50 border-b border-neutral-200 px-4 py-2 flex items-center gap-2 flex-shrink-0">
        <div className="relative flex-1 min-w-0">
          <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-neutral-400 pointer-events-none" />
          <input
            value={filter}
            onChange={e => setFilter(e.target.value)}
            placeholder="Filter…"
            className="w-full pl-7 pr-6 py-1 text-sm border border-neutral-300 rounded bg-white"
          />
          {filter && (
            <button onClick={() => setFilter('')} className="absolute right-2 top-1/2 -translate-y-1/2">
              <X className="w-3.5 h-3.5 text-neutral-400" />
            </button>
          )}
        </div>
        {(['name', 'size', 'date'] as SortKey[]).map(k => (
          <button
            key={k}
            onClick={() => toggleSort(k)}
            className={`text-xs px-2 py-1 rounded border flex-shrink-0 ${sortKey === k ? 'border-blue-400 bg-blue-50 text-blue-700' : 'border-neutral-300 bg-white text-neutral-600'}`}
          >
            {k === 'name' ? 'Name' : k === 'size' ? 'Size' : 'Date'}
            {sortKey === k ? (sortAsc ? ' ↑' : ' ↓') : ''}
          </button>
        ))}
      </div>

      {/* ── Selection / clipboard bar ── */}
      {(selCount > 0 || clipboard) && (
        <div className="bg-blue-50 border-b border-blue-200 px-3 py-2 flex items-center gap-2 flex-shrink-0 flex-wrap text-sm">
          {selCount > 0 && (
            <>
              <span className="text-blue-700 font-medium">{selCount} selected</span>
              <button onClick={() => void downloadSelected()} className="px-2 py-0.5 rounded border border-blue-300 bg-white text-blue-700 hover:bg-blue-100 inline-flex items-center gap-1">
                <Download className="w-3.5 h-3.5" /> Download
              </button>
              <button onClick={copySelected} className="px-2 py-0.5 rounded border border-blue-300 bg-white text-blue-700 hover:bg-blue-100 inline-flex items-center gap-1">
                <Copy className="w-3.5 h-3.5" /> Copy
              </button>
              <button onClick={moveSelected} className="px-2 py-0.5 rounded border border-blue-300 bg-white text-blue-700 hover:bg-blue-100 inline-flex items-center gap-1">
                <Move className="w-3.5 h-3.5" /> Move
              </button>
              {selCount > 1 && (
                <button onClick={openCreateVms} className="px-2 py-0.5 rounded border border-blue-300 bg-white text-blue-700 hover:bg-blue-100 inline-flex items-center gap-1">
                  <Layers className="w-3.5 h-3.5" /> Create VMS
                </button>
              )}
              <button onClick={() => void deleteSelected()} className="px-2 py-0.5 rounded border border-red-300 bg-white text-red-700 hover:bg-red-50 inline-flex items-center gap-1">
                <Trash2 className="w-3.5 h-3.5" /> Delete
              </button>
              <button onClick={() => setSelected(new Set())} className="ml-auto p-1 rounded hover:bg-blue-200">
                <X className="w-3.5 h-3.5 text-blue-600" />
              </button>
            </>
          )}
          {selCount === 0 && clipboard && (
            <>
              <span className="text-blue-700 font-medium">
                {clipboard.op === 'copy' ? 'Copy' : 'Move'} {clipboard.paths.length} item{clipboard.paths.length !== 1 ? 's' : ''} here?
              </span>
              <button onClick={() => void paste()} className="px-3 py-0.5 rounded bg-blue-600 text-white hover:bg-blue-700 inline-flex items-center gap-1">
                <Check className="w-3.5 h-3.5" /> Paste
              </button>
              <button onClick={() => setClipboard(null)} className="ml-auto p-1 rounded hover:bg-blue-200">
                <X className="w-3.5 h-3.5 text-blue-600" />
              </button>
            </>
          )}
        </div>
      )}

      {/* ── File list ── */}
      <div className="flex-1 overflow-y-auto">
        {loading && (
          <div className="p-8 flex flex-col items-center gap-2 text-neutral-500 text-sm">
            <Loader2 className="w-6 h-6 animate-spin" /> Loading…
          </div>
        )}

        {!loading && error && (
          <div className="p-4 text-sm">
            <div className="text-red-600 mb-1">Failed to load directory</div>
            <div className="text-neutral-500 text-xs break-all">{error}</div>
            <button onClick={() => void load(path)} className="mt-3 text-blue-600 inline-flex items-center gap-1">
              <RefreshCw className="w-3 h-3" /> Retry
            </button>
          </div>
        )}

        {!loading && !error && (
          <>
            {path !== '/' && (
              <button
                onClick={navigateUp}
                className="w-full pl-[14px] pr-4 py-3 flex items-center gap-3 border-b border-neutral-100 border-l-2 border-l-transparent transition-colors hover:bg-blue-50 hover:border-l-blue-400 text-left"
              >
                <div className="w-4 flex-shrink-0" />
                <ArrowLeft className="w-5 h-5 text-neutral-400 flex-shrink-0" />
                <span className="text-neutral-600">..</span>
              </button>
            )}

            {visible.length > 0 && (
              <div className="px-4 py-1.5 flex items-center gap-3 border-b border-neutral-100 bg-neutral-50 text-xs text-neutral-500">
                <input
                  type="checkbox"
                  checked={selCount === visible.length && visible.length > 0}
                  onChange={selectAll}
                  className="w-4 h-4"
                />
                <span>{visible.length} item{visible.length !== 1 ? 's' : ''}{filter ? ' (filtered)' : ''}</span>
              </div>
            )}

            {visible.map(entry => (
              <MediaEntry
                key={entry.path}
                entry={entry}
                selected={selected.has(entry.path)}
                onPrimaryClick={() => {
                  if (renameEntry !== null) return;
                  if (entry.type === 'folder') navigateTo(joinPath(path, entry.name));
                  else setMountEntry(entry);
                }}
                onActionsClick={e => { e.stopPropagation(); if (renameEntry !== null) return; setActionEntry(entry); }}
                leftSlot={
                  <input
                    type="checkbox"
                    checked={selected.has(entry.path)}
                    onChange={() => toggleSelect(entry.path)}
                    onClick={e => e.stopPropagation()}
                    className="w-4 h-4 flex-shrink-0"
                  />
                }
                nameSlot={renameEntry?.path === entry.path ? (
                  <input
                    ref={renameInputRef}
                    value={renameName}
                    onChange={e => setRenameName(e.target.value)}
                    onKeyDown={e => {
                      if (e.key === 'Enter') void commitRename();
                      if (e.key === 'Escape') setRenameEntry(null);
                    }}
                    onBlur={() => void commitRename()}
                    onClick={e => e.stopPropagation()}
                    onFocus={e => {
                      const dot = renameName.lastIndexOf('.');
                      e.currentTarget.setSelectionRange(0, dot > 0 ? dot : renameName.length);
                    }}
                    className="w-full px-1 py-0 border border-blue-400 rounded text-sm"
                    autoFocus
                  />
                ) : undefined}
              />
            ))}

            {visible.length === 0 && entries.length === 0 && (
              <div className="p-8 text-center text-neutral-500 text-sm">Empty folder — drop files here to upload</div>
            )}
            {visible.length === 0 && entries.length > 0 && (
              <div className="p-8 text-center text-neutral-500 text-sm">No files match the filter</div>
            )}
          </>
        )}
      </div>

      {/* ── Drag overlay ── */}
      {dragOver && (
        <div className="absolute inset-0 bg-blue-200/60 flex items-center justify-center pointer-events-none z-40">
          <div className="bg-white rounded-xl shadow-lg px-8 py-6 flex flex-col items-center gap-3 text-blue-700">
            <Upload className="w-10 h-10" />
            <span className="font-medium">Drop to upload to {path}</span>
          </div>
        </div>
      )}

      {/* ── Actions modal (per-entry + current-folder context) ── */}
      <ActionsModal
        entry={folderActionOpen
          ? { name: splitPath(path).name || '/', path, type: 'folder', size: 0, lastModified: null, contentType: null }
          : actionEntry}
        onClose={() => { setActionEntry(null); setFolderActionOpen(false); }}
        onOpen={(e, mode) => void openEntry(e, mode)}
        onMount={e => setMountEntry(e)}
        onDownload={e => void downloadEntry(e)}
        onDuplicate={e => void duplicateEntry(e)}
        onRename={e => startRename(e)}
        onCopy={e => cutOrCopyEntry(e, 'copy')}
        onCut={e => cutOrCopyEntry(e, 'move')}
        onDelete={e => void deleteEntry(e)}
        folderManagement={folderActionOpen ? {
          onMountFolder: () => setMountEntry({ name: splitPath(path).name || '/', path, type: 'folder', size: 0, lastModified: null, contentType: null }),
          onConfigureFolder: () => void handleConfigureFolder(),
          onNewFolder: () => { setShowNewFolder(true); setShowNewFile(false); },
          onNewFile:   () => { setShowNewFile(true);   setShowNewFolder(false); },
          onUpload:    () => fileInputRef.current?.click(),
          clipboard,
          onPaste:     () => void paste(),
          selectedCount: selected.size,
          totalCount:    visible.length,
          onSelectAll:   selectAll,
          isRoot: path === (rootPath ?? '/'),
        } : undefined}
      />

      {/* ── File viewer overlay (lazy-loaded) ── */}
      {viewEntry && viewMode && (
        <Suspense fallback={
          <div className="fixed inset-0 bg-neutral-950 z-50 flex items-center justify-center">
            <Loader2 className="w-6 h-6 animate-spin text-neutral-400" />
          </div>
        }>
          <MediaViewerEditor
            entry={viewEntry}
            mode={viewMode}
            availableModes={availableViewers(viewEntry)}
            text={viewText}
            imgUrl={viewImgUrl}
            hexData={viewHexData}
            loading={viewLoading}
            onClose={closeViewer}
            onSwitchMode={m => void switchViewMode(m)}
            onSave={saveViewFile}
            onDownload={() => void downloadEntry(viewEntry)}
          />
        </Suspense>
      )}

      <ConfirmDialog
        open={confirm !== null}
        {...(confirm ?? { title: '', onConfirm: () => {} })}
        onCancel={() => setConfirm(null)}
      />

      {/* ── Mount dialog ── */}
      <Dialog open={mountEntry !== null} onOpenChange={open => !open && setMountEntry(null)}>
        <DialogContent className="max-w-sm flex flex-col max-h-[90dvh]">
          <DialogHeader className="flex-shrink-0">
            <DialogTitle>Mount on Virtual Drive</DialogTitle>
            <DialogDescription title={mountEntry?.name}>
              <MarqueeText>{mountEntry?.name}</MarqueeText>
            </DialogDescription>
          </DialogHeader>
          <div className="overflow-y-auto flex-1 min-h-0">
          {(() => {
            const allDevices = Object.entries(config?.devices?.iec ?? {});
            const drives = allDevices
              .filter(([, v]: [string, any]) => (v as any)?.type === 'drive')
              .map(([k, v]: [string, any]) => ({ type: 'drive' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled }));
            const meatloafs = allDevices
              .filter(([, v]: [string, any]) => (v as any)?.type === 'meatloaf')
              .map(([k, v]: [string, any]) => ({ type: 'meatloaf' as const, key: k, base_url: v?.base_url as string | undefined, url: v?.url as string | undefined, enabled: !!v?.enabled }));
            const devices = [...drives, ...meatloafs];
            if (!devices.length)
              return <p className="text-sm text-neutral-500 text-center py-4">No drive devices found in config.</p>;
            return (
              <div className="flex flex-col gap-2">
                {devices.map(dev => (
                  <button
                    key={`${dev.type}-${dev.key}`}
                    onClick={() => void mountOnDevice(dev.type, dev.key)}
                    className="w-full text-left px-4 py-3 rounded border border-neutral-200 hover:bg-blue-50 hover:border-blue-300 flex items-center gap-3"
                  >
                    <HardDrive className={`w-5 h-5 flex-shrink-0 ${dev.enabled ? 'text-blue-500' : 'text-neutral-400'}`} />
                    <div className="min-w-0 flex-1">
                      <div className="font-medium text-sm">Device #{dev.key}</div>
                      {(dev.base_url || dev.url) && (() => {
                        const displayUrl = [dev.base_url, dev.url]
                          .filter(Boolean).join('');
                        return (
                          <div
                            className="text-xs text-neutral-500 overflow-hidden whitespace-nowrap"
                            style={{ direction: 'rtl', textOverflow: 'ellipsis' }}
                            title={displayUrl}
                          >
                            {/* LTR embed prevents BiDi from reordering the leading '/' to the visual right */}
                            <span style={{ direction: 'ltr', unicodeBidi: 'embed' }}>{displayUrl}</span>
                          </div>
                        );
                      })()}
                    </div>
                    {!dev.enabled && <span className="text-xs text-neutral-400 flex-shrink-0">disabled</span>}
                  </button>
                ))}
              </div>
            );
          })()}
          </div>
        </DialogContent>
      </Dialog>
    </div>
  );
}
