// frontend/src/Uploader.tsx
import React from 'react'
import { api, WhoAmI } from './api'
import * as tus from 'tus-js-client'

type FileRow = { name: string; path: string; is_dir: boolean; size: number; mtime: number }
type Sel = { file: File; desc: string; date: string; finalName: string; progress?: number; err?: string }

console.log('[Pegasus] FE bundle activated at', new Date().toISOString())

// ---------- helpers ----------
function sanitizeDesc(s: string) {
  s = s.trim().replace(/\s+/g, '_').replace(/[^A-Za-z0-9._-]+/g, '_')
  if (!s) s = 'upload'
  return s.slice(0, 64)
}
function sanitizeFolderName(s: string) {
  // 1 level only; convert whitespace -> underscores; allow [A-Za-z0-9_.-]
  s = s.trim().replace(/[\/]+/g, '/').replace(/^\//, '').replace(/\/.*$/, '')
  s = s.replace(/\s+/g, '_').replace(/[^\w.\-]/g, '_').replace(/_+/g, '_')
  return s.slice(0, 64)
}
function extOf(n: string) {
  const i = n.lastIndexOf('.')
  return i > -1 ? n.slice(i + 1).toLowerCase() : ''
}
function stemOf(n: string) {
  const i = n.lastIndexOf('.')
  const stem = i > -1 ? n.slice(0, i) : n
  // keep safe charset and avoid extra dots within segments
  return sanitizeDesc(stem.replace(/\./g, '_')) || 'file'
}
function composeName(date: string, desc: string, orig: string) {
  const d = date || new Date().toISOString().slice(0, 10)
  const [Y, M, D] = d.split('-')
  const sDesc = sanitizeDesc(desc)
  const sStem = stemOf(orig)
  const ext = extOf(orig) || 'bin'
  // New pattern: YYYY.MM.DD.Description.OriginalStem.ext
  return `${Y}.${M}.${D}.${sDesc}.${sStem}.${ext}`
}
function clampOneLevel(p: string) {
  if (!p) return ''
  return p.replace(/^\/+|\/+$/g, '').split('/')[0] || ''
}
function normalizeRows(listRaw: any[]): FileRow[] {
  return (Array.isArray(listRaw) ? listRaw : []).map((r: any) => ({
    name: r?.name ?? r?.Name ?? '',
    path: r?.path ?? r?.Path ?? '',
    is_dir: Boolean(r?.is_dir ?? r?.IsDir ?? r?.isDir ?? false),
    size: Number(r?.size ?? r?.Size ?? 0),
    mtime: Number(r?.mtime ?? r?.Mtime ?? 0),
  }))
}
const videoExt = new Set(['mp4', 'mkv', 'mov', 'avi', 'm4v', 'webm', 'mpg', 'mpeg', 'ts', 'm2ts'])
const imageExt = new Set(['jpg', 'jpeg', 'png', 'gif', 'heic', 'heif', 'webp', 'bmp', 'tif', 'tiff'])
const extLower = (n: string) => (n.includes('.') ? n.split('.').pop()!.toLowerCase() : '')
const isVideoFile = (f: File) => f.type.startsWith('video/') || videoExt.has(extLower(f.name))
const isImageFile = (f: File) => f.type.startsWith('image/') || imageExt.has(extLower(f.name))

function isDetailedError(e: unknown): e is tus.DetailedError {
  return typeof e === 'object' && e !== null && ('originalRequest' in (e as any) || 'originalResponse' in (e as any))
}

function isLikelyMobileUA(): boolean {
  if (typeof window === 'undefined') return false
  const ua = navigator.userAgent || ''
  const coarse = window.matchMedia && window.matchMedia('(pointer: coarse)').matches
  return coarse || /Mobi|Android|iPhone|iPad|iPod/i.test(ua)
}
function useIsMobile(): boolean {
  const [mobile, setMobile] = React.useState(false)
  React.useEffect(() => {
    setMobile(isLikelyMobileUA())
  }, [])
  return mobile
}

// Disable tus resume completely (v2 API: addUpload/removeUpload/listUploads)
const NoResumeUrlStorage: any = {
  addUpload: async (_u: any) => {},
  removeUpload: async (_u: any) => {},
  listUploads: async () => [],
  findUploadsByFingerprint: async (_fp: string) => [],
}
const NoResumeFingerprint = async () => `noresume-${Date.now()}-${Math.random().toString(36).slice(2)}`

// ---------- thumbnail ----------
function PreviewThumb({ file, size = 96 }: { file: File; size?: number }) {
  const [url, setUrl] = React.useState<string>()
  React.useEffect(() => {
    const u = URL.createObjectURL(file)
    setUrl(u)
    return () => {
      try {
        URL.revokeObjectURL(u)
      } catch {}
    }
  }, [file])
  if (!url) return null
  const baseStyle: React.CSSProperties = { width: size, height: size, objectFit: 'cover', borderRadius: 12 }
  if (isImageFile(file)) return <img src={url} alt="" style={baseStyle} className="preview-thumb" />
  if (isVideoFile(file)) return <video src={url} muted preload="metadata" playsInline style={baseStyle} className="preview-thumb" />
  return (
    <div style={{ ...baseStyle, display: 'grid', placeItems: 'center' }} className="preview-thumb">
      📄
    </div>
  )
}

// ---------- component ----------
export default function Uploader() {
  const mobile = useIsMobile()

  const [me, setMe] = React.useState<WhoAmI | undefined>()
  const [libs, setLibs] = React.useState<string[]>([])
  const [lib, setLib] = React.useState<string>('') // required to upload

  const [sub, setSub] = React.useState<string>('')
  const [rootDirs, setRootDirs] = React.useState<string[]>([])
  const [rows, setRows] = React.useState<FileRow[]>([])

  const [status, setStatus] = React.useState<string>('')
  const [globalDate, setGlobalDate] = React.useState<string>(new Date().toISOString().slice(0, 10))
  const [uploading, setUploading] = React.useState<boolean>(false)
  const [sel, setSel] = React.useState<Sel[]>([])
  const [bulkDesc, setBulkDesc] = React.useState<string>('') // helper: apply to all videos
  const folderInputRef = React.useRef<HTMLInputElement>(null)

  // keep raw input for folder name (let user type anything; sanitize on create)
  const [newFolderRaw, setNewFolderRaw] = React.useState('')

  React.useEffect(() => {
    const el = folderInputRef.current
    if (!el) return
    if (mobile) {
      el.removeAttribute('webkitdirectory')
      el.removeAttribute('directory')
    } else {
      el.setAttribute('webkitdirectory', '')
      el.setAttribute('directory', '')
    }
  }, [mobile])

  // initial load
  React.useEffect(() => {
    ;(async () => {
      try {
        setStatus('Loading profile…')
        const m = await api<WhoAmI>('/api/whoami')
        setMe(m as any)
        const mm: any = m
        const L: string[] =
          Array.isArray(mm?.roots) ? mm.roots : Array.isArray(mm?.libs) ? mm.libs : typeof mm?.root === 'string' && mm.root ? [mm.root] : []
        setLibs(L)
        setLib('') // do NOT auto-pick; user will choose
        setSub('')
        setStatus('Choose a library to start')
      } catch (e: any) {
        const msg = String(e?.message || e || '')
        setStatus(`Profile error: ${msg}`)
        if (msg.toLowerCase().includes('no mapping')) {
          alert('Your account is not linked to any upload library yet. Please contact the admin to be granted access.')
          try {
            await api('/api/logout', { method: 'POST' })
          } catch {}
          location.replace('/')
        }
      }
    })()
  }, [])

  React.useEffect(() => {
    ;(async () => {
      if (!lib) {
        setRootDirs([])
        setRows([])
        setSub('')
        return
      }
      try {
        setStatus(`Loading library “${lib}”…`)
        await refresh(lib, sub)
      } catch (e: any) {
        console.error('[Pegasus] refresh error', e)
        setStatus(`List error: ${e?.message || e}`)
      }
    })()
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [lib])

  async function refresh(currLib: string, currSub: string) {
    if (!currLib) return
    const one = clampOneLevel(currSub)
    const listRoot = normalizeRows(await api<any[]>(`/api/list?${new URLSearchParams({ lib: currLib, path: '' })}`))
    const rootDirNames = listRoot
      .filter((e) => e.is_dir)
      .map((e) => e.name)
      .filter(Boolean)
    setRootDirs(rootDirNames.sort((a, b) => a.localeCompare(b)))
    const subOk = one && rootDirNames.includes(one) ? one : ''
    if (subOk !== currSub) setSub(subOk)

    const path = subOk ? subOk : ''
    const list = subOk ? await api<any[]>(`/api/list?${new URLSearchParams({ lib: currLib, path })}`) : listRoot
    setRows(normalizeRows(list))

    const show = `/${[currLib, path].filter(Boolean).join('/')}`
    setStatus(`Ready · Destination: ${show}`)
  }

  function handleChoose(files: FileList) {
    const arr = Array.from(files).map((f) => {
      const base = (f as any).webkitRelativePath || f.name
      const name = base.split('/').pop() || f.name
      const desc = '' // start empty; required later for videos only
      return { file: f, desc, date: globalDate, finalName: composeName(globalDate, sanitizeDesc(desc), name), progress: 0 }
    })
    console.log('[Pegasus] selected files', arr.map((a) => a.file.name))
    setSel(arr)
  }

  // recompute finalName when global date changes
  React.useEffect(() => {
    setSel((old) => old.map((x) => ({ ...x, date: globalDate, finalName: composeName(globalDate, x.desc, x.file.name) })))
  }, [globalDate])

  // Warn before closing mid-upload
  React.useEffect(() => {
    const handler = (e: BeforeUnloadEvent) => {
      if (uploading) {
        e.preventDefault()
        e.returnValue = ''
      }
    }
    window.addEventListener('beforeunload', handler)
    return () => window.removeEventListener('beforeunload', handler)
  }, [uploading])

  // Apply description to all videos helper
  function applyDescToAllVideos() {
    if (!bulkDesc.trim()) return
    setSel((old) =>
      old.map((x) => (isVideoFile(x.file) ? { ...x, desc: bulkDesc, finalName: composeName(x.date, bulkDesc, x.file.name) } : x))
    )
  }

  async function doUpload() {
    if (!me) {
      setStatus('Not signed in')
      return
    }
    if (!lib) {
      alert('Please select a Library to upload into.')
      return
    }
    // Require description only for videos:
    const missingVideos = sel.filter((s) => isVideoFile(s.file) && !s.desc.trim()).length
    if (missingVideos > 0) {
      alert(`Please add a short description for all videos. ${missingVideos} video(s) missing a description.`)
      return
    }

    setStatus('Starting upload…')
    setUploading(true)
    try {
      for (const s of sel) {
        // eslint-disable-next-line no-await-in-loop
        await new Promise<void>((resolve, reject) => {
          const opts: tus.UploadOptions & { withCredentials?: boolean } = {
            endpoint: '/tus/',
            chunkSize: 5 * 1024 * 1024,
            retryDelays: [0, 1000, 3000, 5000, 10000],
            metadata: {
              filename: s.file.name,
              lib: lib,
              subdir: sub || '',
              date: s.date,
              desc: s.desc, // server enforces: required for videos only
            },
            onError: (err: Error | tus.DetailedError) => {
              let msg = String(err)
              if (isDetailedError(err)) {
                const status = (err.originalRequest as any)?.status
                const statusText = (err.originalRequest as any)?.statusText
                if (status) msg = `HTTP ${status} ${statusText || ''}`.trim()
              }
              console.error('[Pegasus] tus error', s.file.name, err)
              setSel((old) => old.map((x) => (x.file === s.file ? { ...x, err: msg } : x)))
              setStatus(`✖ ${s.file.name}: ${msg}`)
              alert(`Upload failed: ${s.file.name}\n\n${msg}`)
              reject(err as any)
            },
            onProgress: (sent: number, total: number) => {
              const pct = Math.floor((sent / Math.max(total, 1)) * 100)
              setSel((old) => old.map((x) => (x.file === s.file ? { ...x, progress: pct } : x)))
              setStatus(`⬆ ${s.finalName}: ${pct}%`)
            },
            onSuccess: () => {
              setSel((old) => old.map((x) => (x.file === s.file ? { ...x, progress: 100, err: undefined } : x)))
              setStatus(`✔ ${s.finalName} uploaded`)
              console.log('[Pegasus] tus success', s.file.name)
              resolve()
            },
          }
          opts.withCredentials = true
          ;(opts as any).urlStorage = NoResumeUrlStorage
          ;(opts as any).fingerprint = NoResumeFingerprint

          const up = new tus.Upload(s.file, opts)
          console.log('[Pegasus] tus.start', { name: s.file.name, size: s.file.size, type: s.file.type, lib, sub })
          up.start()
        })
      }
      setStatus('All uploads complete')
      setSel([])
      await refresh(lib, sub)
    } finally {
      setUploading(false)
    }
  }

  // -------- one-level subfolder ops --------
  async function createSubfolder(nameRaw: string) {
    const name = sanitizeFolderName(nameRaw)
    if (!name) return
    try {
      await api(`/api/mkdir`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ lib: lib, path: name }),
      })
      await refresh(lib, name) // jump into new folder
      setSub(name)
    } catch (e: any) {
      console.error('[Pegasus] mkdir error', e)
      alert(`Create folder failed:\n${e?.message || e}`)
    }
  }
  async function renameFolder(oldName: string) {
    const nn = prompt('New folder name:', oldName)
    const newName = sanitizeFolderName(nn || '')
    if (!newName || newName === oldName) return
    try {
      await api('/api/rename', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ lib: lib, from: oldName, to: newName }),
      })
      const newSub = sub === oldName ? newName : sub
      setSub(newSub)
      await refresh(lib, newSub)
    } catch (e: any) {
      console.error('[Pegasus] rename folder error', e)
      alert(`Rename failed:\n${e?.message || e}`)
    }
  }
  async function deleteFolder(name: string) {
    if (!confirm(`Delete folder “${name}” (and its contents)?`)) return
    try {
      await api(`/api/file?${new URLSearchParams({ lib: lib, path: name, recursive: 'true' })}`, { method: 'DELETE' })
      const newSub = sub === name ? '' : sub
      setSub(newSub)
      await refresh(lib, newSub)
    } catch (e: any) {
      console.error('[Pegasus] delete folder error', e)
      alert(`Delete failed:\n${e?.message || e}`)
    }
  }

  // destination listing (actions: rename files only at library root)
  async function renamePath(oldp: string) {
    const base = oldp.split('/').pop() || ''
    const name = prompt('New name (YYYY.MM.DD.Description.OrigStem.ext):', base)
    if (!name) return
    try {
      await api('/api/rename', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ lib: lib, from: oldp, to: oldp.split('/').slice(0, -1).concat(sanitizeFolderName(name)).join('/') }),
      })
      await refresh(lib, sub)
    } catch (e: any) {
      console.error('[Pegasus] rename error', e)
      alert(`Rename failed:\n${e?.message || e}`)
    }
  }
  async function deletePath(p: string, recursive: boolean) {
    if (!confirm(`Delete ${p}${recursive ? ' (recursive)' : ''}?`)) return
    try {
      await api(`/api/file?${new URLSearchParams({ lib: lib, path: p, recursive: recursive ? 'true' : 'false' })}`, {
        method: 'DELETE',
      })
      await refresh(lib, sub)
    } catch (e: any) {
      console.error('[Pegasus] delete error', e)
      alert(`Delete failed:\n${e?.message || e}`)
    }
  }

  // sort rows
  const sortedRows = React.useMemo(() => {
    const arr = Array.isArray(rows) ? rows.slice() : []
    return arr.sort((a, b) => {
      const dirFirst = Number(b?.is_dir ? 1 : 0) - Number(a?.is_dir ? 1 : 0)
      if (dirFirst !== 0) return dirFirst
      const an = a?.name ?? ''
      const bn = b?.name ?? ''
      return an.localeCompare(bn)
    })
  }, [rows])

  // Existing filenames in the destination (to block collisions)
  const existingNames = React.useMemo(() => new Set(sortedRows.filter((r) => !r.is_dir).map((r) => r.name)), [sortedRows])

  // Duplicates inside this batch
  const duplicateNamesInSelection = React.useMemo(() => {
    const counts = new Map<string, number>()
    sel.forEach((s) => counts.set(s.finalName, (counts.get(s.finalName) || 0) + 1))
    return new Set(Array.from(counts.entries()).filter(([, c]) => c > 1).map(([n]) => n))
  }, [sel])

  const hasNameIssues = React.useMemo(
    () => sel.some((s) => duplicateNamesInSelection.has(s.finalName) || existingNames.has(s.finalName)),
    [sel, duplicateNamesInSelection, existingNames]
  )

  const destPath = `/${[lib, sub].filter(Boolean).join('/')}`
  const videosNeedingDesc = sel.filter((s) => isVideoFile(s.file) && !s.desc.trim()).length

  return (
    <>
      {/* Header context */}
      <section>
        <hgroup>
          <h2>Signed in: {me?.username}</h2>
          <p className="meta">
            Destination: <strong>{destPath || '/(choose a library)'}</strong>
          </p>
        </hgroup>
      </section>

      {/* Choose content */}
      <section>
        <h3>Choose content</h3>
        <div className="grid-3">
          <label>
            Default date
            <input type="date" value={globalDate} onChange={(e) => setGlobalDate(e.target.value)} />
          </label>
        </div>

        <div className="file-picker">
          {mobile ? (
            <>
              <label>
                Gallery/Photos
                <input type="file" multiple accept="image/*,video/*" onChange={(e) => e.target.files && handleChoose(e.target.files)} />
              </label>
              <label>
                Camera (optional)
                <input type="file" accept="image/*,video/*" capture="environment" onChange={(e) => e.target.files && handleChoose(e.target.files)} />
              </label>
            </>
          ) : (
            <>
              <label>
                Select file(s)
                <input type="file" multiple accept="image/*,video/*" onChange={(e) => e.target.files && handleChoose(e.target.files)} />
              </label>
              <label>
                Select folder(s)
                <input type="file" multiple ref={folderInputRef} onChange={(e) => e.target.files && handleChoose(e.target.files)} />
              </label>
            </>
          )}
        </div>
      </section>

      {/* Review */}
      <section>
        <h3>Review Files</h3>

        {sel.length === 0 ? (
          <p className="meta">Select at least one file.</p>
        ) : (
          <>
            <article>
              <div style={{ display: 'grid', gap: 12 }}>
                <label>
                  Description for all videos (optional)
                  <input placeholder="Short video description" value={bulkDesc} onChange={(e) => setBulkDesc(e.target.value)} />
                </label>
                <button type="button" className="stretch" onClick={applyDescToAllVideos} disabled={!bulkDesc.trim()}>
                  Apply to all videos
                </button>
                {videosNeedingDesc > 0 ? (
                  <small className="meta bad">{videosNeedingDesc} video(s) need a description</small>
                ) : (
                  <small className="meta mono-wrap">All videos have descriptions</small>
                )}
              </div>
            </article>

            {sel.map((s, i) => (
              <article key={i} className="video-card">
                <div className="thumb-row">
                  <PreviewThumb file={s.file} size={isVideoFile(s.file) ? 176 : 128} />
                </div>

                <h4 className="filename wrap-anywhere" title={s.file.name}>
                  {s.file.name}
                </h4>

                <label>
                  {isVideoFile(s.file) ? 'Short description (required for video)' : 'Description (optional for image)'}
                  <input
                    required={isVideoFile(s.file)}
                    aria-invalid={isVideoFile(s.file) && !s.desc.trim()}
                    placeholder={isVideoFile(s.file) ? 'Required for video' : 'Optional for image'}
                    value={s.desc}
                    onChange={(e) => {
                      const v = e.target.value
                      setSel((old) => old.map((x, idx) => (idx === i ? { ...x, desc: v, finalName: composeName(x.date, v, x.file.name) } : x)))
                    }}
                  />
                </label>

                <label>
                  Date
                  <input
                    type="date"
                    value={s.date}
                    onChange={(e) => {
                      const v = e.target.value
                      setSel((old) => old.map((x, idx) => (idx === i ? { ...x, date: v, finalName: composeName(v, x.desc, x.file.name) } : x)))
                    }}
                  />
                </label>

                <small className="meta" title={s.finalName}>
                  → {s.finalName}
                </small>
                {typeof s.progress === 'number' && <progress max={100} value={s.progress}></progress>}
                {s.err && <small className="meta bad">Error: {s.err}</small>}
                {duplicateNamesInSelection.has(s.finalName) && (
                  <small className="meta bad">Duplicate name in this batch. Adjust description or date.</small>
                )}
                {existingNames.has(s.finalName) && <small className="meta bad">A file with this name already exists here.</small>}
              </article>
            ))}
          </>
        )}
      </section>

      {/* Destination */}
      <section>
        <h3>Select Destination</h3>
        <p className="meta">Manage a library & choose a destination.</p>

        {/* Library only (required). No subfolder dropdown. */}
        <div className="grid-3">
          <label>
            Library (required)
            <select
              value={lib}
              onChange={(e) => {
                const v = e.target.value
                setLib(v)
                setSub('')
                if (v) void refresh(v, '')
              }}
            >
              <option value="">— Select a library —</option>
              {libs.map((L) => (
                <option key={L} value={L}>
                  {L}
                </option>
              ))}
            </select>
          </label>
        </div>

        {!lib ? (
          <p className="meta">Select a library to create subfolders and view contents.</p>
        ) : (
          <>
            {/* New subfolder: free typing; sanitize on create */}
            <div className="grid-3">
              <label style={{ gridColumn: '1 / -1' }}>
                Add a new subfolder (optional):
                <input
                  placeholder="letters, numbers, underscores, dashes"
                  value={newFolderRaw}
                  onChange={(e) => setNewFolderRaw(e.target.value)}
                  inputMode="text"
                />
              </label>

              {newFolderRaw.trim() && (
                <div className="row-center" style={{ gridColumn: '1 / -1', marginTop: 8 }}>
                  <button
                    type="button"
                    onClick={() => {
                      void createSubfolder(newFolderRaw)
                      setNewFolderRaw('')
                    }}
                  >
                    Create
                  </button>
                </div>
              )}
            </div>

            {/* Current selection with actions; includes Home button to go to root */}
            <article className="card">
              <div className="row-between" style={{ marginBottom: 6 }}>
                <div>
                  <span className="meta">Current Sub-Folder:</span> <strong className="wrap-anywhere">{sub || '(library root)'}</strong>
                </div>
              </div>
              {sub && (
                <div className="row-center buttons-row">
                  <button
                    className="icon-btn"
                    title="Go to library root"
                    aria-label="Go to library root"
                    onClick={() => { setSub(''); void refresh(lib, '') }}
                  >
                    🏠
                  </button>
                  <button onClick={() => void renameFolder(sub)}>Rename</button>
                  <button onClick={() => void deleteFolder(sub)} className="bad">Delete</button>
                  <button onClick={() => { setSub(''); void refresh(lib, '') }}>Clear</button>
                </div>
              )}
            </article>

            {/* Subfolders: only Select (right), name + icon (left); clamp width to avoid horizontal overflow */}
            <details open style={{ marginTop: 8 }}>
              <summary>Subfolders</summary>
              {rootDirs.length === 0 ? (
                <p className="meta">
                  No subfolders yet. You’ll upload into <b>/{lib}</b>.
                </p>
              ) : (
                <div style={{ display: 'grid', gap: 8 }}>
                  {rootDirs.map((d) => (
                    <article key={d} className="list-row">
                      <div className="name" style={{ flex: '1 1 auto', minWidth: 0 }}>
                        <div className="wrap-anywhere" title={d}>📁 {d}</div>
                      </div>
                      <div className="row-right">
                        <button
                          onClick={() => {
                            setSub(d)
                            void refresh(lib, d)
                          }}
                        >
                          Select
                        </button>
                      </div>
                    </article>
                  ))}
                </div>
              )}
            </details>

            {/* Contents: info left (clamped), actions right */}
            <details open style={{ marginTop: 8 }}>
              <summary>Contents of {destPath || '/(choose)'}</summary>
              {sortedRows.length === 0 ? (
                <p className="meta">Empty.</p>
              ) : (
                <div style={{ display: 'grid', gap: 8 }}>
                  {sortedRows.map((f) => (
                    <article key={f.path} className="list-row">
                      <div className="name" style={{ flex: '1 1 auto', minWidth: 0 }}>
                        <div className="wrap-anywhere" title={f.name || '(unnamed)'}>{f.is_dir ? '📁' : '🎞️'} {f.name || '(unnamed)'}</div>
                        <small className="meta meta-wrap">{f.is_dir ? 'folder' : fmt(f.size)} · {f.mtime ? new Date(f.mtime * 1000).toLocaleString() : ''}</small>
                      </div>
                      <div className="row-right">
                        {f.is_dir ? (
                          <button
                            onClick={() => {
                              setSub(f.name)
                              void refresh(lib, f.name)
                            }}
                          >
                            Open
                          </button>
                        ) : (
                          <>
                            {sub === '' && <button onClick={() => void renamePath(f.path)}>Rename</button>}
                            <button onClick={() => void deletePath(f.path, false)} className="bad">
                              Delete
                            </button>
                          </>
                        )}
                      </div>
                    </article>
                  ))}
                </div>
              )}
            </details>
          </>
        )}
      </section>

      <footer style={{ display: 'flex', gap: 12, alignItems: 'center', justifyContent: 'space-between', marginTop: 8 }}>
        <div style={{ display: 'flex', flexDirection: 'column' }}>
          <small className="meta" aria-live="polite">
            {status}
          </small>
          {hasNameIssues && <small className="meta bad">Resolve duplicate/existing filename conflicts to enable Upload.</small>}
        </div>
        <button
          type="button"
          onClick={() => {
            const disabled = !lib || !sel.length || uploading || videosNeedingDesc > 0 || hasNameIssues
            if (!disabled) void doUpload()
          }}
          disabled={!lib || !sel.length || uploading || videosNeedingDesc > 0 || hasNameIssues}
        >
          Upload {sel.length ? `(${sel.length})` : ''}
        </button>
      </footer>
    </>
  )
}

function fmt(n: number) {
  if (n < 1024) return n + ' B'
  const u = ['KB', 'MB', 'GB', 'TB']
  let i = -1
  do {
    n /= 1024
    i++
  } while (n >= 1024 && i < u.length - 1)
  return n.toFixed(1) + ' ' + u[i]
}
