// frontend\src\components\ui\FinishedDownloads.tsx

'use client'

import * as React from 'react'
import { useMemo, useEffect, useCallback } from 'react'
import Card from './Card'
import type { RecordJob } from '../../types'
import ButtonGroup from './ButtonGroup'
import {
  TableCellsIcon,
  RectangleStackIcon,
  Squares2X2Icon,
  AdjustmentsHorizontalIcon,
} from '@heroicons/react/24/outline'
import { type SwipeCardHandle } from './SwipeCard'
import { flushSync } from 'react-dom'
import FinishedDownloadsCardsView from './FinishedDownloadsCardsView'
import FinishedDownloadsTableView from './FinishedDownloadsTableView'
import FinishedDownloadsGalleryView from './FinishedDownloadsGalleryView'
import Pagination from './Pagination'
import { applyInlineVideoPolicy } from './videoPolicy'
import TagBadge from './TagBadge'
import Button from './Button'
import { useNotify } from './notify'
import { isHotName, stripHotPrefix } from './hotName'
import LabeledSwitch from './LabeledSwitch'
import Switch from './Switch'
import LoadingSpinner from './LoadingSpinner'

type SortMode =
  | 'completed_desc'
  | 'completed_asc'
  | 'file_asc'
  | 'file_desc'
  | 'duration_desc'
  | 'duration_asc'
  | 'size_desc'
  | 'size_asc'

type TeaserPlaybackMode = 'still' | 'hover' | 'all'

type Props = {
  jobs: RecordJob[]
  doneJobs: RecordJob[]
  modelsByKey: Record<string, StoredModelFlags>
  blurPreviews?: boolean
  teaserPlayback?: TeaserPlaybackMode
  teaserAudio?: boolean
  onOpenPlayer: (job: RecordJob, startAtSec?: number) => void
  onDeleteJob?: (
    job: RecordJob
  ) => void | { undoToken?: string } | Promise<void | { undoToken?: string }>
  onToggleHot?: (
    job: RecordJob
  ) => void | { ok?: boolean; oldFile?: string; newFile?: string } | Promise<void | { ok?: boolean; oldFile?: string; newFile?: string }>
  onToggleFavorite?: (job: RecordJob) => void | Promise<void>
  onToggleLike?: (job: RecordJob) => void | Promise<void>
  onToggleWatch?: (job: RecordJob) => void | Promise<void>
  onKeepJob?: (job: RecordJob) => void | Promise<void>
  onSplitJob?: (job: RecordJob) => void | Promise<void>
  doneTotal: number
  page: number
  pageSize: number
  onPageChange: (page: number) => void
  assetNonce?: number
  sortMode: SortMode
  onSortModeChange: (m: SortMode) => void
  loadMode?: 'paged' | 'all'
}

const norm = (p: string) => (p || '').replaceAll('\\', '/')

const baseName = (p: string) => {
  const n = norm(p)
  const parts = n.split('/')
  return parts[parts.length - 1] || ''
}

const keyFor = (j: RecordJob) => {
  // ✅ Primär: stabile Job-ID (bleibt gleich bei Rename/HOT)
  const id = (j as any)?.id
  if (id != null && String(id).trim() !== '') return String(id)

  // Fallback (sollte selten sein)
  const f = baseName(j.output || '')
  return f || String((j as any)?.output || '')
}

const isTrashOutput = (output?: string) => {
  const p = norm(String(output ?? ''))
  // match: ".../.trash/file.ext" oder "...\ .trash\file.ext"
  return p.includes('/.trash/') || p.endsWith('/.trash')
}

function formatDuration(ms: number): string {
  if (!Number.isFinite(ms) || ms <= 0) return '—'
  const totalSec = Math.floor(ms / 1000)
  const h = Math.floor(totalSec / 3600)
  const m = Math.floor((totalSec % 3600) / 60)
  const s = totalSec % 60
  if (h > 0) return `${h}h ${m}m`
  if (m > 0) return `${m}m ${s}s`
  return `${s}s`
}

function formatBytes(bytes?: number | null): string {
  if (typeof bytes !== 'number' || !Number.isFinite(bytes) || bytes <= 0) return '—'
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
  let v = bytes
  let i = 0
  while (v >= 1024 && i < units.length - 1) {
    v /= 1024
    i++
  }
  const digits = i === 0 ? 0 : v >= 100 ? 0 : v >= 10 ? 1 : 2
  return `${v.toFixed(digits)} ${units[i]}`
}

function useMediaQuery(query: string) {
  const [matches, setMatches] = React.useState(false)

  useEffect(() => {
    const mql = window.matchMedia(query)
    const onChange = () => setMatches(mql.matches)
    onChange()

    if (mql.addEventListener) mql.addEventListener('change', onChange)
    else mql.addListener(onChange)

    return () => {
      if (mql.removeEventListener) mql.removeEventListener('change', onChange)
      else mql.removeListener(onChange)
    }
  }, [query])

  return matches
}

const modelNameFromOutput = (output?: string) => {
  const fileRaw = baseName(output || '')
  const file = stripHotPrefix(fileRaw)
  if (!file) return '—'

  const stem = file.replace(/\.[^.]+$/, '')
  const m = stem.match(/^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}$/)
  if (m?.[1]) return m[1]

  const i = stem.lastIndexOf('_')
  return i > 0 ? stem.slice(0, i) : stem
}

type StoredModelFlags = {
  id: string
  modelKey: string
  favorite?: boolean
  liked?: boolean | null
  watching?: boolean | null
  tags?: string
}

const lower = (s: string) => (s || '').trim().toLowerCase()

// Tags kommen aus dem ModelStore als String (meist komma-/semicolon-getrennt)
const parseTags = (raw?: string): string[] => {
  const s = String(raw ?? '').trim()
  if (!s) return []
  const parts = s
    .split(/[\n,;|]+/g)
    .map((p) => p.trim())
    .filter(Boolean)

  // stable dedupe (case-insensitive), aber original casing behalten
  const seen = new Set<string>()
  const out: string[] = []
  for (const p of parts) {
    const k = p.toLowerCase()
    if (seen.has(k)) continue
    seen.add(k)
    out.push(p)
  }

  // wie in ModelsTab: alphabetisch (case-insensitive)
  out.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
  return out
}

// liest “irgendein” Size-Feld (falls du eins hast) aus dem Job
const sizeBytesOf = (job: RecordJob): number | null => {
  const anyJob = job as any
  const v =
    anyJob.sizeBytes ??
    anyJob.fileSizeBytes ??
    anyJob.bytes ??
    anyJob.size ??
    null
  return typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : null
}

function sleep(ms: number) {
  return new Promise<void>((resolve) => window.setTimeout(resolve, ms))
}

function errorTextOf(err: unknown): string {
  if (err instanceof Error) return err.message || String(err)
  return String(err ?? '')
}

function looksLikeFileInUseError(err: unknown): boolean {
  const s = errorTextOf(err).toLowerCase()
  return (
    s.includes('wird gerade verwendet') ||
    s.includes('wird gerade abgespielt') ||
    s.includes('sharing violation') ||
    s.includes('used by another process') ||
    s.includes('file in use') ||
    s.includes('409')
  )
}

async function fetchWithTextError(input: RequestInfo | URL, init?: RequestInit) {
  const res = await fetch(input, init)
  if (!res.ok) {
    const text = await res.text().catch(() => '')
    const msg = (text || `HTTP ${res.status}`).trim()
    throw new Error(msg)
  }
  return res
}

type QueuedMutationTask = {
  id: string
  run: () => Promise<void>
}

function useMutationQueue() {
  const queueRef = React.useRef<QueuedMutationTask[]>([])
  const runningRef = React.useRef(false)
  const scheduledRef = React.useRef(false)

  // verhindert Doppel-Klick/Doppel-Swipe auf dieselbe Aktion
  const pendingIdsRef = React.useRef<Set<string>>(new Set())

  const schedulePump = React.useCallback(() => {
    if (scheduledRef.current) return
    scheduledRef.current = true

    const kick = () => {
      scheduledRef.current = false
      void pump()
    }

    // best-effort "im Hintergrund"
    if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
      ;(window as any).requestIdleCallback(kick, { timeout: 250 })
    } else {
      setTimeout(kick, 0)
    }
  }, [])

  const pump = React.useCallback(async () => {
    if (runningRef.current) return
    runningRef.current = true

    try {
      while (queueRef.current.length > 0) {
        const task = queueRef.current.shift()
        if (!task) continue

        try {
          await task.run()
        } finally {
          pendingIdsRef.current.delete(task.id)
        }

        // Yield zwischen Tasks -> UI bleibt responsiver
        await new Promise<void>((resolve) => setTimeout(resolve, 0))
      }
    } finally {
      runningRef.current = false

      // falls währenddessen neue Tasks reinkamen
      if (queueRef.current.length > 0) {
        schedulePump()
      }
    }
  }, [schedulePump])

  const enqueue = React.useCallback((id: string, run: () => Promise<void>) => {
    if (!id) return false
    if (pendingIdsRef.current.has(id)) return false

    pendingIdsRef.current.add(id)
    queueRef.current.push({ id, run })
    schedulePump()
    return true
  }, [schedulePump])

  const isQueued = React.useCallback((id: string) => {
    return pendingIdsRef.current.has(id)
  }, [])

  return { enqueue, isQueued }
}

export default function FinishedDownloads({ 
  jobs,
  doneJobs,
  blurPreviews,
  teaserPlayback,
  teaserAudio,
  onOpenPlayer,
  onDeleteJob,
  onToggleHot,
  onToggleFavorite,
  onToggleLike,
  onToggleWatch,
  onKeepJob,
  onSplitJob,
  doneTotal,
  page,
  pageSize,
  onPageChange,
  assetNonce,
  sortMode,
  onSortModeChange,
  modelsByKey,
  loadMode = 'paged',
 }: Props) {
  const allMode = loadMode === 'all'

  const teaserPlaybackMode: TeaserPlaybackMode = teaserPlayback ?? 'hover'

  // Desktop vs Mobile: Desktop (hoverfähiger Pointer) -> Teaser nur bei Hover, Mobile -> im Viewport
  const canHover = useMediaQuery('(hover: hover) and (pointer: fine)')

  const notify = useNotify()

  const mutationQueue = useMutationQueue()

  const teaserHostsRef = React.useRef<Map<string, HTMLElement>>(new Map())
  const [teaserKey, setTeaserKey] = React.useState<string | null>(null)
  const [hoverTeaserKey, setHoverTeaserKey] = React.useState<string | null>(null)

  const teaserIORef = React.useRef<IntersectionObserver | null>(null)
  const elToKeyRef = React.useRef<WeakMap<Element, string>>(new WeakMap())

  // 🗑️ lokale Optimistik: sofort ausblenden, ohne auf das nächste Polling zu warten
  const [deletedKeys, setDeletedKeys] = React.useState<Set<string>>(() => new Set())
  const [deletingKeys, setDeletingKeys] = React.useState<Set<string>>(() => new Set())

  const [isLoading, setIsLoading] = React.useState(false)

  const refillInFlightRef = React.useRef(false)
  const refillQueuedWhileInFlightRef = React.useRef(false)

  // ✅ schützt gegen alte Effect-Instanzen / StrictMode-Cleanup / Race Conditions
  const refillSessionRef = React.useRef(0)

  type UndoAction =
  | { kind: 'delete'; undoToken: string; originalFile: string; rowKey?: string; from?: 'done' | 'keep' }
  | { kind: 'keep'; keptFile: string; originalFile: string; rowKey?: string }
  | { kind: 'hot'; currentFile: string }

  type PersistedUndoState = {
    v: 1
    action: UndoAction
    ts: number
  }

  const LAST_UNDO_KEY = 'finishedDownloads_lastUndo_v1'

  const [lastAction, setLastAction] = React.useState<UndoAction | null>(() => {
    if (typeof window === 'undefined') return null
    try {
      const raw = localStorage.getItem(LAST_UNDO_KEY)
      if (!raw) return null

      const parsed = JSON.parse(raw) as PersistedUndoState
      if (!parsed || parsed.v !== 1 || !parsed.action) return null

      // optional TTL (z. B. 30 min), damit kein uraltes Undo angezeigt wird
      const ageMs = Date.now() - Number(parsed.ts || 0)
      if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs > 30 * 60 * 1000) {
        localStorage.removeItem(LAST_UNDO_KEY)
        return null
      }

      return parsed.action
    } catch {
      return null
    }
  })
  const [undoing, setUndoing] = React.useState(false)

  useEffect(() => {
    try {
      if (!lastAction) {
        localStorage.removeItem(LAST_UNDO_KEY)
        return
      }

      const payload: PersistedUndoState = {
        v: 1,
        action: lastAction,
        ts: Date.now(),
      }

      localStorage.setItem(LAST_UNDO_KEY, JSON.stringify(payload))
    } catch {
      // ignore
    }
  }, [lastAction])

  // 🔥 lokale Optimistik: HOT Rename sofort in der UI spiegeln
  const [renamedFiles, setRenamedFiles] = React.useState<Record<string, string>>({})

  // 📄 Pagination-Refill: nach Delete/Keep Seite neu laden, damit Items "nachrücken"
  const [overrideDoneJobs, setOverrideDoneJobs] = React.useState<RecordJob[] | null>(null)
  const [overrideDoneTotal, setOverrideDoneTotal] = React.useState<number | null>(null)
  const [refillTick, setRefillTick] = React.useState(0)
  const refillTimerRef = React.useRef<number | null>(null)

  const countHintRef = React.useRef({ pending: 0, t: 0, timer: 0 as any })

  const emitCountHint = React.useCallback((delta: number) => {
    if (!delta || !Number.isFinite(delta)) return

    countHintRef.current.pending += delta

    // coalesce mehrere Aktionen sehr kurz hintereinander
    if (countHintRef.current.timer) return

    countHintRef.current.timer = window.setTimeout(() => {
      countHintRef.current.timer = 0
      const d = countHintRef.current.pending
      countHintRef.current.pending = 0
      if (!d) return

      window.dispatchEvent(
        new CustomEvent('finished-downloads:count-hint', { detail: { delta: d } })
      )
    }, 120)
  }, [])

  type ViewMode = 'table' | 'cards' | 'gallery'
  const VIEW_KEY = 'finishedDownloads_view'
  const KEEP_KEY = 'finishedDownloads_includeKeep_v2'
  const MOBILE_OPTS_KEY = 'finishedDownloads_mobileOptionsOpen_v1'

  const [view, setView] = React.useState<ViewMode>('table')
  const [includeKeep, setIncludeKeep] = React.useState(false)

  const [mobileOptionsOpen, setMobileOptionsOpen] = React.useState(false)

  const swipeRefs = React.useRef<Map<string, SwipeCardHandle>>(new Map())

  // 🏷️ Tag-Filter (klickbare Tags wie in ModelsTab)
  const [tagFilter, setTagFilter] = React.useState<string[]>([])
  const activeTagSet = useMemo(() => new Set(tagFilter.map(lower)), [tagFilter])

  const modelTags = useMemo(() => {
    const tagsByModelKey: Record<string, string[]> = {}
    const tagSetByModelKey: Record<string, Set<string>> = {}

    for (const [k, flags] of Object.entries(modelsByKey ?? {})) {
      const key = lower(k)
      const arr = parseTags(flags?.tags)
      tagsByModelKey[key] = arr
      tagSetByModelKey[key] = new Set(arr.map(lower))
    }

    return { tagsByModelKey, tagSetByModelKey }
  }, [modelsByKey])

  // 🔎 Suche (client-side, innerhalb der aktuell geladenen Seite)
  const [searchQuery, setSearchQuery] = React.useState<string>('')
  const deferredSearchQuery = React.useDeferredValue(searchQuery)

  const searchTokens = useMemo(
    () => lower(deferredSearchQuery).split(/\s+/g).map((s) => s.trim()).filter(Boolean),
    [deferredSearchQuery]
  )

  const clearSearch = useCallback(() => setSearchQuery(''), [])

  const toggleTagFilter = useCallback((tag: string) => {
    const k = lower(tag)
    setTagFilter((prev) => {
      const has = prev.some((t) => lower(t) === k)
      return has ? prev.filter((t) => lower(t) !== k) : [...prev, tag]
    })
  }, [])

  // ✅ Mobile/UX: globales "all=1" erst bei sinnvoller Suche triggern
  const searchActiveForGlobalFetch =
    activeTagSet.size > 0 ||
    searchTokens.some((t) => t.length >= 2)

  const globalFilterActive = searchActiveForGlobalFetch
  const effectiveAllMode = globalFilterActive || allMode

  const fetchAllDoneJobs = useCallback(
    async (signal?: AbortSignal) => {
      // ✅ Nur sichtbares Loading zeigen, wenn wir noch keine Override-Daten haben
      const shouldShowLoading = overrideDoneJobs == null
      if (shouldShowLoading) setIsLoading(true)
      try {
        const res = await fetch(
          `/api/record/done?all=1&sort=${encodeURIComponent(sortMode)}&withCount=1${includeKeep ? '&includeKeep=1' : ''}`,
          {
            cache: 'no-store' as any,
            signal,
          }
        )
        if (!res.ok) return

        const data = await res.json().catch(() => null)
        const items = Array.isArray(data?.items) ? (data.items as RecordJob[]) : []
        const count = Number(data?.count ?? data?.totalCount ?? items.length)

        setOverrideDoneJobs(items)
        setOverrideDoneTotal(Number.isFinite(count) ? count : items.length)
      } finally {
        if (shouldShowLoading) setIsLoading(false)
      }
    },
    [sortMode, includeKeep]
  )

  const clearTagFilter = useCallback(() => setTagFilter([]), [])

  useEffect(() => {
    // ✅ Wenn wir aus CategoriesTab kommen, liegt evtl. ein "pending" Filter in localStorage
    try {
      const raw = localStorage.getItem('finishedDownloads_pendingTags')
      if (!raw) return

      const arr = JSON.parse(raw)
      const tags = Array.isArray(arr) ? arr.map((t) => String(t || '').trim()).filter(Boolean) : []
      if (tags.length === 0) return

      flushSync(() => setTagFilter(tags))
      if (page !== 1) onPageChange(1)
    } catch {
      // ignore
    } finally {
      try {
        localStorage.removeItem('finishedDownloads_pendingTags')
      } catch {}
    }
  }, []) // nur einmal beim Mount

  const queueRefill = useCallback(() => {
    // ✅ Schon geplant? Dann nicht nochmal planen.
    if (refillTimerRef.current != null) return

    refillTimerRef.current = window.setTimeout(() => {
      refillTimerRef.current = null
      setRefillTick((n) => n + 1)
    }, 80)
  }, [page, pageSize, sortMode, includeKeep])

  useEffect(() => {
    if (!effectiveAllMode) return

    const ac = new AbortController()

    const t = window.setTimeout(() => {
      fetchAllDoneJobs(ac.signal).catch(() => {})
    }, 250)

    return () => {
      window.clearTimeout(t)
      ac.abort()
    }
  }, [effectiveAllMode, fetchAllDoneJobs])

  // ✅ Seite auffüllen + doneTotal aktualisieren, damit Pagination stimmt
  useEffect(() => {
    if (refillTick === 0) return

    const ac = new AbortController()
    let alive = true
    let finished = false // ✅ pro Effect-Instanz idempotent

    // ✅ eigene Session-ID für diese Effect-Instanz
    const mySession = ++refillSessionRef.current

    const finishRefill = () => {
      if (finished) return
      finished = true

      // ✅ Nur die AKTUELLE Session darf den globalen Refill-Status verändern
      if (refillSessionRef.current !== mySession) {
        return
      }

      refillInFlightRef.current = false

      if (refillQueuedWhileInFlightRef.current) {
        refillQueuedWhileInFlightRef.current = false
        queueRefill()
      }
    }

    // ✅ Refill läuft (nur wenn diese Session noch aktuell ist)
    if (refillSessionRef.current === mySession) {
      refillInFlightRef.current = true
    }

    // ✅ Wenn Filter aktiv: nicht paginiert ziehen, sondern "all"
    if (effectiveAllMode) {
      ;(async () => {
        try {
          // fetchAllDoneJobs setzt isLoading selbst
          await fetchAllDoneJobs(ac.signal)
          if (alive) {
            refillRetryRef.current = 0
          }
        } catch {
          // ignore (Abort/Netzwerk)
        } finally {
          if (alive) finishRefill()
        }
      })()

      return () => {
        alive = false
        ac.abort()
        finishRefill()
      }
    }

    // ✅ paged refill → hier Loading setzen
    setIsLoading(true)

    ;(async () => {
      try {
        // 1) Liste + optional count in EINEM Request holen
        const listRes = await fetch(
          `/api/record/done?page=${page}&pageSize=${pageSize}&sort=${encodeURIComponent(sortMode)}&withCount=1${
            includeKeep ? '&includeKeep=1' : ''
          }`,
          { cache: 'no-store' as any, signal: ac.signal }
        )

        if (!alive || ac.signal.aborted) return

        const okAll = listRes.ok

        if (listRes.ok) {
          const data = await listRes.json().catch(() => null)
          if (!alive || ac.signal.aborted) return

          const items = Array.isArray(data?.items)
            ? (data.items as RecordJob[])
            : Array.isArray(data)
              ? data
              : []

          setOverrideDoneJobs(items)

          // ✅ Count direkt aus /done lesen (withCount=1)
          const countRaw = Number(data?.count ?? data?.totalCount ?? items.length)
          const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0

          setOverrideDoneTotal(count)

          const totalPages = Math.max(1, Math.ceil(count / pageSize))
          if (page > totalPages) {
            onPageChange(totalPages)
            setOverrideDoneJobs(null)
            return
          }
        }

        if (okAll) {
          refillRetryRef.current = 0
        } else if (alive && !ac.signal.aborted && refillRetryRef.current < 2) {
          refillRetryRef.current += 1
          const retryNo = refillRetryRef.current

          window.setTimeout(() => {
            if (!ac.signal.aborted) {
              setRefillTick((n) => n + 1)
            }
          }, 400 * retryNo)
        }
      } catch {
        // Abort / Fehler ignorieren
      } finally {
        if (alive) {
          setIsLoading(false)
          finishRefill()
        }
      }
    })()

    return () => {
      alive = false
      ac.abort()
      finishRefill()
    }
  }, [
    refillTick,
    effectiveAllMode,
    fetchAllDoneJobs,
    page,
    pageSize,
    sortMode,
    includeKeep,
    onPageChange,
  ])

  useEffect(() => {
    // Override-Daten nur dann zurücksetzen, wenn wir wirklich wieder auf
    // die normalen /done-Props zurückfallen wollen.
    if (effectiveAllMode || includeKeep) return

    setOverrideDoneJobs(null)
    setOverrideDoneTotal(null)
  }, [page, pageSize, sortMode, includeKeep, effectiveAllMode])
  
  useEffect(() => {
    if (effectiveAllMode) return

    // Nur nötig, wenn wir NICHT auf die normalen Props zurückfallen.
    if (!includeKeep) return

    const ac = new AbortController()
    let alive = true

    setIsLoading(true)

    ;(async () => {
      try {
        const res = await fetch(
          `/api/record/done?page=${page}&pageSize=${pageSize}&sort=${encodeURIComponent(sortMode)}&withCount=1&includeKeep=1`,
          { cache: 'no-store' as any, signal: ac.signal }
        )
        if (!res.ok || !alive || ac.signal.aborted) return

        const data = await res.json().catch(() => null)
        if (!alive || ac.signal.aborted) return

        const items = Array.isArray(data?.items) ? (data.items as RecordJob[]) : []
        const countRaw = Number(data?.count ?? data?.totalCount ?? items.length)
        const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : items.length

        setOverrideDoneJobs(items)
        setOverrideDoneTotal(count)
      } finally {
        if (alive) setIsLoading(false)
      }
    })()

    return () => {
      alive = false
      ac.abort()
    }
  }, [effectiveAllMode, includeKeep, page, pageSize, sortMode])

  useEffect(() => {
    try {
      const saved = localStorage.getItem(VIEW_KEY) as ViewMode | null
      if (saved === 'table' || saved === 'cards' || saved === 'gallery') {
        setView(saved)
      } else {
        // Default: Mobile -> Cards, sonst Tabelle
        setView(window.matchMedia('(max-width: 639px)').matches ? 'cards' : 'table')
      }
    } catch {
      setView('table')
    }
  }, [])

  useEffect(() => {
    try {
      localStorage.setItem(VIEW_KEY, view)
    } catch {}
  }, [view])

    useEffect(() => {
    try {
      const raw = localStorage.getItem(KEEP_KEY)
      setIncludeKeep(raw === '1' || raw === 'true' || raw === 'yes')
    } catch {
      setIncludeKeep(false)
    }
  }, [])

  useEffect(() => {
    try {
      localStorage.setItem(KEEP_KEY, includeKeep ? '1' : '0')
    } catch {}
  }, [includeKeep])

  useEffect(() => {
    try {
      const raw = localStorage.getItem(MOBILE_OPTS_KEY)
      setMobileOptionsOpen(raw === '1' || raw === 'true' || raw === 'yes')
    } catch {
      setMobileOptionsOpen(false)
    }
  }, [])

  useEffect(() => {
    try {
      localStorage.setItem(MOBILE_OPTS_KEY, mobileOptionsOpen ? '1' : '0')
    } catch {}
  }, [mobileOptionsOpen])

  // 🔹 hier sammeln wir die Videodauer pro Job/Datei (Sekunden)
  const [durations, setDurations] = React.useState<Record<string, number>>({})

  // ✅ Perf: durations gesammelt flushen (verhindert viele Re-renders beim initialen Preview-Mount)
  const durationsRef = React.useRef<Record<string, number>>({})
  const durationsFlushTimerRef = React.useRef<number | null>(null)

  // 🔹 hier sammeln wir die Videoauflösung pro Job/Datei
  const [resolutions, setResolutions] = React.useState<Record<string, { w: number; h: number }>>({})

  // ✅ Perf: resolutions gesammelt flushen (wie durations)
  const resolutionsRef = React.useRef<Record<string, { w: number; h: number }>>({})
  const resolutionsFlushTimerRef = React.useRef<number | null>(null)

  const refillRetryRef = React.useRef(0)

  React.useEffect(() => {
    resolutionsRef.current = resolutions
  }, [resolutions])

  const flushResolutionsSoon = React.useCallback(() => {
    if (resolutionsFlushTimerRef.current != null) return
    resolutionsFlushTimerRef.current = window.setTimeout(() => {
      resolutionsFlushTimerRef.current = null
      setResolutions({ ...resolutionsRef.current })
    }, 200)
  }, [])

  React.useEffect(() => {
    return () => {
      if (resolutionsFlushTimerRef.current != null) {
        window.clearTimeout(resolutionsFlushTimerRef.current)
        resolutionsFlushTimerRef.current = null
      }
    }
  }, [])

  React.useEffect(() => {
    durationsRef.current = durations
  }, [durations])

  const flushDurationsSoon = React.useCallback(() => {
    if (durationsFlushTimerRef.current != null) return
    durationsFlushTimerRef.current = window.setTimeout(() => {
      durationsFlushTimerRef.current = null
      // neue Objekt-Referenz, damit React aktualisiert
      setDurations({ ...durationsRef.current })
    }, 200)
  }, [])

  React.useEffect(() => {
    return () => {
      if (durationsFlushTimerRef.current != null) {
        window.clearTimeout(durationsFlushTimerRef.current)
        durationsFlushTimerRef.current = null
      }
    }
  }, [])

  const [inlinePlay, setInlinePlay] = React.useState<{ key: string; nonce: number } | null>(null)

  const previewMuted = !Boolean(teaserAudio)

  const tryAutoplayInline = useCallback((domId: string) => {
    const host = document.getElementById(domId)
    const v = host?.querySelector('video') as HTMLVideoElement | null
    if (!v) return false

    applyInlineVideoPolicy(v, { muted: previewMuted })

    const p = v.play?.()
    if (p && typeof (p as any).catch === 'function') (p as Promise<void>).catch(() => {})
    return true
  }, [previewMuted])

  const startInline = useCallback((key: string) => {
    setInlinePlay((prev) => (prev?.key === key ? { key, nonce: prev.nonce + 1 } : { key, nonce: 1 }))
  }, [])

  const startInlineAt = useCallback((key: string, seconds: number, domId: string) => {
    const safeSeconds = Number.isFinite(seconds) && seconds > 0 ? seconds : 0

    // Inline-Preview aktivieren / remount erzwingen
    setInlinePlay((prev) => (prev?.key === key ? { key, nonce: prev.nonce + 1 } : { key, nonce: 1 }))

    // Nach dem Rendern das Video suchen, seeken und autoplay versuchen
    const trySeekAndPlay = (retriesLeft: number) => {
      const host = document.getElementById(domId)
      const v = host?.querySelector('video') as HTMLVideoElement | null

      if (!v) {
        if (retriesLeft > 0) {
          requestAnimationFrame(() => trySeekAndPlay(retriesLeft - 1))
        }
        return
      }

      applyInlineVideoPolicy(v, { muted: previewMuted })

      const applySeek = () => {
        try {
          const dur = Number(v.duration)
          const maxSeek =
            Number.isFinite(dur) && dur > 0
              ? Math.max(0, dur - 0.05)
              : safeSeconds

          v.currentTime = Math.max(0, Math.min(safeSeconds, maxSeek))
        } catch {
          // ignore
        }

        const p = v.play?.()
        if (p && typeof (p as any).catch === 'function') {
          ;(p as Promise<void>).catch(() => {})
        }
      }

      // Wenn Metadaten schon da sind -> direkt seeken
      if (v.readyState >= 1) {
        applySeek()
        return
      }

      // Sonst warten bis metadata da sind
      const onLoadedMetadata = () => {
        v.removeEventListener('loadedmetadata', onLoadedMetadata)
        applySeek()
      }

      v.addEventListener('loadedmetadata', onLoadedMetadata, { once: true })

      // zusätzlich sofort play versuchen (hilft manchmal)
      const p = v.play?.()
      if (p && typeof (p as any).catch === 'function') {
        ;(p as Promise<void>).catch(() => {})
      }
    }

    requestAnimationFrame(() => trySeekAndPlay(8))
  }, [previewMuted])

  const openPlayer = useCallback((job: RecordJob) => {
    setInlinePlay(null)
    onOpenPlayer(job)
  }, [onOpenPlayer])

  const openPlayerAt = useCallback((job: RecordJob, seconds: number) => {
    const s = Number.isFinite(seconds) && seconds >= 0 ? seconds : 0
    setInlinePlay(null)
    onOpenPlayer(job, s)
  }, [onOpenPlayer])

  const handleScrubberClickIndex = useCallback(
    (job: RecordJob, segmentIndex: number, segmentCount: number) => {
      const idx = Number.isFinite(segmentIndex) ? Math.floor(segmentIndex) : 0
      const count = Number.isFinite(segmentCount) ? Math.floor(segmentCount) : 0

      if (count <= 0) {
        // Fallback: Player normal öffnen
        openPlayer(job)
        return
      }

      // Dauer bevorzugt aus Preview-Metadaten, sonst aus Job
      const k = keyFor(job)
      const durationSec =
        durations[k] ??
        ((job as any)?.durationSeconds as number | undefined) ??
        0

      if (!Number.isFinite(durationSec) || durationSec <= 0) {
        // Wenn keine Dauer bekannt ist: trotzdem öffnen (ohne Timestamp)
        openPlayer(job)
        return
      }

      // Segment-Index -> Startsekunde
      // Beispiel: 10 Segmente, Klick auf Index 0..9
      const clampedIdx = Math.max(0, Math.min(idx, count - 1))
      const secPerSegment = durationSec / count
      const startAtSec = clampedIdx * secPerSegment

      openPlayerAt(job, startAtSec)
    },
    [durations, keyFor, openPlayer, openPlayerAt]
  )

  const markDeleting = useCallback((key: string, value: boolean) => {
    setDeletingKeys((prev) => {
      const next = new Set(prev)
      if (value) next.add(key)
      else next.delete(key)
      return next
    })
  }, [])

  const markDeleted = useCallback((key: string) => {
    setDeletedKeys((prev) => {
      const next = new Set(prev)
      next.add(key)
      return next
    })
  }, [])

  const [keepingKeys, setKeepingKeys] = React.useState<Set<string>>(() => new Set())

  const markKeeping = useCallback((key: string, value: boolean) => {
    setKeepingKeys((prev) => {
      const next = new Set(prev)
      if (value) next.add(key)
      else next.delete(key)
      return next
    })
  }, [])

  // neben deletedKeys / deletingKeys
  const [removingKeys, setRemovingKeys] = React.useState<Set<string>>(() => new Set())

  // ⏱️ Timer pro Key, damit wir Optimistik bei Fehler sauber zurückrollen können
  const removeTimersRef = React.useRef<Map<string, number>>(new Map())

  useEffect(() => {
    return () => {
      for (const t of removeTimersRef.current.values()) {
        window.clearTimeout(t)
      }
      removeTimersRef.current.clear()
    }
  }, [])

  const markRemoving = useCallback((key: string, value: boolean) => {
    setRemovingKeys((prev) => {
      const next = new Set(prev)
      if (value) next.add(key)
      else next.delete(key)
      return next
    })
  }, [])

  const cancelRemoveTimer = useCallback((key: string) => {
    const t = removeTimersRef.current.get(key)
    if (t != null) {
      window.clearTimeout(t)
      removeTimersRef.current.delete(key)
    }
  }, [])

  const restoreRow = useCallback(
    (key: string) => {
      // Timer stoppen (falls die "commit delete"-Phase noch aussteht)
      cancelRemoveTimer(key)

      // wieder sichtbar machen
      setDeletedKeys((prev) => {
        const next = new Set(prev)
        next.delete(key)
        return next
      })
      setRemovingKeys((prev) => {
        const next = new Set(prev)
        next.delete(key)
        return next
      })
      setDeletingKeys((prev) => {
        const next = new Set(prev)
        next.delete(key)
        return next
      })
      setKeepingKeys((prev) => {
        const next = new Set(prev)
        next.delete(key)
        return next
      })
    },
    [cancelRemoveTimer]
  )

  const animateRemove = useCallback(
    (key: string) => {
      markRemoving(key, true)
      cancelRemoveTimer(key)

      const t = window.setTimeout(() => {
        removeTimersRef.current.delete(key)

        markDeleted(key)
        markRemoving(key, false)
      }, 320)

      removeTimersRef.current.set(key, t)
    },
    [markDeleted, markRemoving, cancelRemoveTimer]
  )

  const releasePlayingFile = useCallback(
    async (file: string, opts?: { close?: boolean }) => {
      // 1) App-/Overlay-Player freigeben
      window.dispatchEvent(new CustomEvent('player:release', { detail: { file } }))
      if (opts?.close) {
        window.dispatchEvent(new CustomEvent('player:close', { detail: { file } }))
      }

      // 2) Einmal auf den nächsten Frame warten (React/DOM cleanup)
      await new Promise<void>((r) => requestAnimationFrame(() => r()))

      // 3) Nochmals release senden (hilft bei race zwischen close/unmount)
      window.dispatchEvent(new CustomEvent('player:release', { detail: { file } }))

      // 4) Windows/Filesystem braucht manchmal einen Moment bis Handles wirklich frei sind
      await new Promise((r) => window.setTimeout(r, 260))
    },
    []
  )

  const withFileReleaseRetry = useCallback(
    async <T,>(
      file: string,
      run: () => Promise<T>,
      opts?: { close?: boolean; attempts?: number; baseDelayMs?: number }
    ): Promise<T> => {
      const attempts = Math.max(1, opts?.attempts ?? 4)
      const baseDelayMs = Math.max(50, opts?.baseDelayMs ?? 220)

      let lastErr: unknown

      for (let attempt = 1; attempt <= attempts; attempt++) {
        try {
          // vor JEDEM Versuch freigeben (nicht nur einmal)
          await releasePlayingFile(file, { close: opts?.close ?? true })

          // kurzer Tick extra (DOM/video cleanup, OS handle release)
          if (attempt > 1) {
            await sleep(baseDelayMs * attempt)
          } else {
            await sleep(80)
          }

          return await run()
        } catch (e) {
          lastErr = e

          // nur bei "Datei in Verwendung" retryen
          if (!looksLikeFileInUseError(e) || attempt >= attempts) {
            throw e
          }

          // nächster Versuch
          continue
        }
      }

      throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? 'Unbekannter Fehler'))
    },
    [releasePlayingFile]
  )

  type FileMutationKind = 'delete' | 'keep' | 'rename'

  type RunFileMutationOptions<T> = {
    kind: FileMutationKind
    job: RecordJob
    file: string
    rowKey: string

    // UI / State
    setBusy?: (v: boolean) => void
    isBusyNow?: () => boolean
    optimisticRemove?: boolean
    alreadyRemoved?: boolean

    // Ausführung
    run: () => Promise<T>

    // Hooks
    onSuccess?: (result: T) => Promise<void> | void
    onError?: (err: unknown) => Promise<void> | void

    // Messages
    labels: {
      invalidTitle: string
      invalidBody: string
      inUseTitle: string
      failTitle: string
      failPrefix?: string
    }
  }

  const runFileMutation = useCallback(
    async <T,>(opts: RunFileMutationOptions<T>): Promise<{ ok: boolean; result?: T }> => {
      const {
        file,
        rowKey,
        setBusy,
        isBusyNow,
        optimisticRemove,
        alreadyRemoved,
        run,
        onSuccess,
        onError,
        labels,
      } = opts

      if (!file) {
        notify.error(labels.invalidTitle, labels.invalidBody)
        return { ok: false }
      }

      if (isBusyNow?.()) return { ok: false }

      setBusy?.(true)

      try {
        if (optimisticRemove && !alreadyRemoved) {
          animateRemove(rowKey)
        }

        const result = await run()
        await onSuccess?.(result)

        return { ok: true, result }
      } catch (e: any) {
        // Optimistik zurückrollen
        if (optimisticRemove) {
          restoreRow(rowKey)
        }

        await onError?.(e)

        if (looksLikeFileInUseError(e)) {
          notify.error(labels.inUseTitle, `${file} wird noch verwendet (Player/Preview). Bitte kurz warten und erneut versuchen.`)
        } else {
          const suffix = e?.message ? ` — ${String(e.message)}` : ''
          notify.error(labels.failTitle, `${labels.failPrefix ?? file}${suffix}`)
        }

        return { ok: false }
      } finally {
        setBusy?.(false)
      }
    },
    [notify, animateRemove, restoreRow]
  )

  const deleteVideo = useCallback(
    async (job: RecordJob, opts?: { alreadyRemoved?: boolean }): Promise<boolean> => {
      const file = baseName(job.output || '')
      const key = keyFor(job)

      const res = await runFileMutation({
        kind: 'delete',
        job,
        file,
        rowKey: key,
        setBusy: (v) => markDeleting(key, v),
        isBusyNow: () => deletingKeys.has(key),
        optimisticRemove: true,
        alreadyRemoved: opts?.alreadyRemoved,
        labels: {
          invalidTitle: 'Löschen nicht möglich',
          invalidBody: 'Kein Dateiname gefunden – kann nicht löschen.',
          inUseTitle: 'Löschen fehlgeschlagen',
          failTitle: 'Löschen fehlgeschlagen',
          failPrefix: file,
        },
        run: async () => {
          if (onDeleteJob) {
            return await withFileReleaseRetry(
              file,
              async () => await onDeleteJob(job),
              { close: true, attempts: 4, baseDelayMs: 220 }
            )
          }

          const r = await withFileReleaseRetry(
            file,
            async () =>
              await fetchWithTextError(`/api/record/delete?file=${encodeURIComponent(file)}`, {
                method: 'POST',
              }),
            { close: true, attempts: 4, baseDelayMs: 220 }
          )

          return (await r.json().catch(() => null)) as any
        },
        onSuccess: async (result: any) => {
          // Fall 1: externer Handler (App) liefert { undoToken }
          if (onDeleteJob) {
            const undoToken = typeof result?.undoToken === 'string' ? result.undoToken : ''
            if (undoToken) {
              setLastAction({ kind: 'delete', undoToken, originalFile: file, rowKey: key })
            } else {
              setLastAction(null)
            }
            return
          }

          // Fall 2: lokaler API-Call (liefert from + undoToken)
          const from = (result?.from === 'keep' ? 'keep' : 'done') as 'done' | 'keep'
          const undoToken = typeof result?.undoToken === 'string' ? result.undoToken : ''

          if (undoToken) {
            setLastAction({ kind: 'delete', undoToken, originalFile: file, rowKey: key, from })
          } else {
            setLastAction(null)
          }

          queueRefill()
          emitCountHint(-1)
        },
      })

      return res.ok
    },
    [
      baseName,
      keyFor,
      deletingKeys,
      markDeleting,
      onDeleteJob,
      withFileReleaseRetry,
      runFileMutation,
      queueRefill,
      emitCountHint,
    ]
  )

  const keepVideo = useCallback(
    async (job: RecordJob, opts?: { alreadyRemoved?: boolean }) => {
      const file = baseName(job.output || '')
      const key = keyFor(job)

      const res = await runFileMutation({
        kind: 'keep',
        job,
        file,
        rowKey: key,
        setBusy: (v) => markKeeping(key, v),
        isBusyNow: () => keepingKeys.has(key) || deletingKeys.has(key),
        optimisticRemove: true,
        alreadyRemoved: opts?.alreadyRemoved,
        labels: {
          invalidTitle: 'Keep nicht möglich',
          invalidBody: 'Kein Dateiname gefunden – kann nicht behalten.',
          inUseTitle: 'Keep fehlgeschlagen',
          failTitle: 'Keep fehlgeschlagen',
          failPrefix: file,
        },
        run: async () => {
          if (onKeepJob) {
            return await withFileReleaseRetry(
              file,
              async () => await onKeepJob(job),
              { close: true, attempts: 4, baseDelayMs: 220 }
            )
          }

          const r = await withFileReleaseRetry(
            file,
            async () =>
              await fetchWithTextError(`/api/record/keep?file=${encodeURIComponent(file)}`, {
                method: 'POST',
              }),
            { close: true, attempts: 4, baseDelayMs: 220 }
          )
          return (await r.json().catch(() => null)) as any
        },
        onSuccess: async (data: any) => {
          const keptFile = typeof data?.newFile === 'string' && data.newFile ? data.newFile : file

          setLastAction({ kind: 'keep', keptFile, originalFile: file, rowKey: key })

          queueRefill()
          emitCountHint(includeKeep ? 0 : -1)
        },
      })

      return res.ok
    },
    [
      baseName,
      keyFor,
      markKeeping,
      keepingKeys,
      deletingKeys,
      withFileReleaseRetry,
      runFileMutation,
      queueRefill,
      emitCountHint,
      includeKeep,
    ]
  )

  const applyRename = useCallback((oldFile: string, newFile: string) => {
    if (!oldFile || !newFile || oldFile === newFile) return
    setRenamedFiles((prev) => {
      const next: Record<string, string> = { ...prev }
      for (const [k, v] of Object.entries(next)) {
        if (k === oldFile || k === newFile || v === oldFile || v === newFile) delete next[k]
      }
      next[oldFile] = newFile
      return next
    })
  }, [])

  const clearRenamePair = useCallback((a: string, b: string) => {
    if (!a && !b) return
    setRenamedFiles((prev) => {
      const next: Record<string, string> = { ...prev }
      for (const [k, v] of Object.entries(next)) {
        if (k === a || k === b || v === a || v === b) delete next[k]
      }
      return next
    })
  }, [])

  const undoLastAction = useCallback(async () => {
    if (!lastAction || undoing) return
    setUndoing(true)

    const unhideByToken = (token: string) => {
      setDeletedKeys((prev) => {
        const next = new Set(prev)
        next.delete(token)
        return next
      })
      setDeletingKeys((prev) => {
        const next = new Set(prev)
        next.delete(token)
        return next
      })
      setKeepingKeys((prev) => {
        const next = new Set(prev)
        next.delete(token)
        return next
      })
      setRemovingKeys((prev) => {
        const next = new Set(prev)
        next.delete(token)
        return next
      })
    }

    try {
      if (lastAction.kind === 'delete') {
        const res = await fetch(
          `/api/record/restore?token=${encodeURIComponent(lastAction.undoToken)}`,
          { method: 'POST' }
        )
        if (!res.ok) {
          const text = await res.text().catch(() => '')
          throw new Error(text || `HTTP ${res.status}`)
        }
        const data = await res.json().catch(() => null) as any
        const restoredFile = String(data?.restoredFile || lastAction.originalFile)

        if (lastAction.rowKey) unhideByToken(lastAction.rowKey)
        // Fallbacks (falls alte lastAction ohne rowKey existiert)
        unhideByToken(lastAction.originalFile)
        unhideByToken(restoredFile)

        const visibleDelta = lastAction.from === 'keep' && !includeKeep ? 0 : +1
        emitCountHint(visibleDelta)
        queueRefill()
        setLastAction(null)
        return
      }

      if (lastAction.kind === 'keep') {
        const res = await fetch(
          `/api/record/unkeep?file=${encodeURIComponent(lastAction.keptFile)}`,
          { method: 'POST' }
        )
        if (!res.ok) {
          const text = await res.text().catch(() => '')
          throw new Error(text || `HTTP ${res.status}`)
        }
        const data = await res.json().catch(() => null) as any
        const restoredFile = String(data?.newFile || lastAction.originalFile)

        if (lastAction.rowKey) unhideByToken(lastAction.rowKey)
        // Fallbacks (für ältere Actions / Sonderfälle)
        unhideByToken(lastAction.originalFile)
        unhideByToken(restoredFile)

        emitCountHint(+1)
        queueRefill()
        setLastAction(null)
        return
      }

      if (lastAction.kind === 'hot') {
        const res = await fetch(
          `/api/record/toggle-hot?file=${encodeURIComponent(lastAction.currentFile)}`,
          { method: 'POST' }
        )
        if (!res.ok) {
          const text = await res.text().catch(() => '')
          throw new Error(text || `HTTP ${res.status}`)
        }

        const data = (await res.json().catch(() => null)) as any
        const oldFile = String(data?.oldFile || lastAction.currentFile)
        const newFile = String(data?.newFile || '')

        if (newFile) {
          applyRename(oldFile, newFile)
        }

        queueRefill()
        setLastAction(null)
        return
      }
    } catch (e: any) {
      notify.error('Undo fehlgeschlagen', String(e?.message || e))
    } finally {
      setUndoing(false)
    }
  }, [
    lastAction,
    undoing,
    notify,
    queueRefill,
    includeKeep,
    emitCountHint,
    applyRename,
  ])

  const [hotBusyKeys, setHotBusyKeys] = React.useState<Set<string>>(() => new Set())

  const markHotBusy = useCallback((key: string, value: boolean) => {
    setHotBusyKeys((prev) => {
      const next = new Set(prev)
      if (value) next.add(key)
      else next.delete(key)
      return next
    })
  }, [])

  const toggleHotVideo = useCallback(
    async (job: RecordJob): Promise<void> => {
      const currentFile = baseName(job.output || '')
      const key = keyFor(job)

      const toggledName = (raw: string) => (isHotName(raw) ? stripHotPrefix(raw) : `HOT ${raw}`)
      const oldFile = currentFile
      const optimisticNew = toggledName(oldFile)

      await runFileMutation({
        kind: 'rename',
        job,
        file: currentFile,
        rowKey: key,
        setBusy: (v) => markHotBusy(key, v),
        isBusyNow: () => hotBusyKeys.has(key),
        optimisticRemove: false,
        labels: {
          invalidTitle: 'HOT nicht möglich',
          invalidBody: 'Kein Dateiname gefunden – kann nicht HOT togglen.',
          inUseTitle: 'HOT umbenennen fehlgeschlagen',
          failTitle: 'HOT umbenennen fehlgeschlagen',
          failPrefix: oldFile,
        },
        run: async () => {
          // Optimistik sofort anwenden
          applyRename(oldFile, optimisticNew)

          if (onToggleHot) {
            const r = await onToggleHot(job)
            return r as any
          }

          const r = await withFileReleaseRetry(
            oldFile,
            async () =>
              await fetchWithTextError(
                `/api/record/toggle-hot?file=${encodeURIComponent(oldFile)}`,
                { method: 'POST' }
              ),
            { close: true, attempts: 4, baseDelayMs: 220 }
          )

          return (await r.json().catch(() => null)) as any
        },
        onSuccess: async (data: any) => {
          const apiOld = typeof data?.oldFile === 'string' && data.oldFile ? data.oldFile : oldFile
          const apiNew = typeof data?.newFile === 'string' && data.newFile ? data.newFile : optimisticNew

          if (apiOld && apiNew && apiOld !== apiNew) {
            applyRename(apiOld, apiNew)
          }

          setLastAction({ kind: 'hot', currentFile: apiNew })

          if (!onToggleHot || sortMode === 'file_asc' || sortMode === 'file_desc') {
            queueRefill()
          }
        },
        onError: async () => {
          // Rename-Optimistik rollback
          clearRenamePair(oldFile, optimisticNew)
          setLastAction(null)
        },
      })
    },
    [
      baseName,
      keyFor,
      hotBusyKeys,
      markHotBusy,
      runFileMutation,
      applyRename,
      clearRenamePair,
      onToggleHot,
      withFileReleaseRetry,
      queueRefill,
      sortMode,
    ]
  )

  const enqueueDeleteVideo = useCallback((job: RecordJob): boolean => {
    const key = keyFor(job)
    const file = baseName(job.output || '')
    if (!key || !file) return false

    // bereits aktiv? dann nicht nochmal
    if (deletingKeys.has(key) || keepingKeys.has(key)) return false

    // sofort visuelles Busy (leichtgewichtig)
    markDeleting(key, true)

    // ✅ sofort raus aus dem Stack (optimistisch)
    animateRemove(key)

    const qid = `delete:${key}`
    const accepted = mutationQueue.enqueue(qid, async () => {
      try {
        await deleteVideo(job, { alreadyRemoved: true })
      } finally {
        // deleteVideo setzt markDeleting(false) selbst im finally,
        // daher hier nichts zusätzlich nötig.
      }
    })

    if (!accepted) {
      restoreRow(key) // ✅ macht markDeleting false + removing/deleted rollback
    }

    return accepted
  }, [
    mutationQueue,
    keyFor,
    baseName,
    deletingKeys,
    keepingKeys,
    markDeleting,
    deleteVideo,
  ])

  const enqueueKeepVideo = useCallback((job: RecordJob): boolean => {
    const key = keyFor(job)
    const file = baseName(job.output || '')
    if (!key || !file) return false

    if (keepingKeys.has(key) || deletingKeys.has(key)) return false

    markKeeping(key, true)

    // ✅ sofort aus dem sichtbaren Stack raus
    animateRemove(key)

    const qid = `keep:${key}`
    const accepted = mutationQueue.enqueue(qid, async () => {
      try {
        await keepVideo(job, { alreadyRemoved: true })
      } finally {
        // keepVideo macht markKeeping(false) im finally
      }
    })

    if (!accepted) {
      restoreRow(key)
    }

    return accepted
  }, [
    mutationQueue,
    keyFor,
    baseName,
    keepingKeys,
    deletingKeys,
    markKeeping,
    keepVideo,
  ])

  const enqueueToggleHotVideo = useCallback((job: RecordJob): boolean => {
    const key = keyFor(job)
    if (!key) return false

    if (hotBusyKeys.has(key)) return false

    const qid = `hot:${key}`
    return mutationQueue.enqueue(qid, async () => {
      await toggleHotVideo(job)
    })
  }, [mutationQueue, keyFor, toggleHotVideo, hotBusyKeys])

  const applyRenamedOutput = useCallback(
    (job: RecordJob): RecordJob => {
      const out = norm(job.output || '')
      const file = baseName(out)
      const override = renamedFiles[file]
      if (!override) return job

      const idx = out.lastIndexOf('/')
      const dir = idx >= 0 ? out.slice(0, idx + 1) : ''
      return { ...job, output: dir + override }
    },
    [renamedFiles]
  )

  const doneJobsPage = overrideDoneJobs ?? doneJobs
  const doneTotalPage = overrideDoneTotal ?? doneTotal

  const rows = useMemo(() => {
    const map = new Map<string, RecordJob>()

    // Basis: Files aus dem Done-Ordner
    for (const j of doneJobsPage) {
      const jj = applyRenamedOutput(j)
      map.set(keyFor(jj), jj)
    }

    // Jobs aus /list drübermergen
    for (const j of jobs) {
      const jj = applyRenamedOutput(j)
      const k = keyFor(jj)
      if (map.has(k)) map.set(k, { ...map.get(k)!, ...jj })
    }

    const list = Array.from(map.values()).filter((j) => {
      if (deletedKeys.has(keyFor(j))) return false

      // ✅ .trash niemals anzeigen
      if (isTrashOutput(j.output)) return false

      const anyJ = j as any
      const phase = String(anyJ?.phase ?? '').trim().toLowerCase()
      const pw = anyJ?.postWork
      const pwState = String(pw?.state ?? '').trim().toLowerCase()

      const isActiveFinishedPostwork =
        phase === 'probe' ||
        phase === 'remuxing' ||
        phase === 'moving' ||
        phase === 'assets' ||
        phase === 'postwork' ||
        pwState === 'queued' ||
        pwState === 'running'

      // ✅ Alles, was noch in Nacharbeiten steckt, NICHT im Finished-Tab zeigen
      if (isActiveFinishedPostwork) return false

      return (
        j.status === 'finished' ||
        j.status === 'failed' ||
        j.status === 'stopped'
      )
    })

    return list
  }, [jobs, doneJobsPage, deletedKeys, applyRenamedOutput])

  const viewRows = rows

  const fileToKeyRef = React.useRef<Map<string, string>>(new Map())

  useEffect(() => {
    const m = new Map<string, string>()
    for (const j of viewRows) {
      const f = baseName(j.output || '')
      if (!f) continue
      m.set(f, keyFor(j))
    }
    fileToKeyRef.current = m
  }, [viewRows])

  useEffect(() => {
    const onExternalDelete = (ev: Event) => {
      const detail = (ev as CustomEvent<{ file: string; phase: 'start'|'success'|'error' }>).detail
      if (!detail?.file) return

      const key = fileToKeyRef.current.get(detail.file) || detail.file

      if (detail.phase === 'start') {
        markDeleting(key, true)

        // ✅ Optimistik: überall gleich -> animiert raus
        animateRemove(key)

        return
      }

      if (detail.phase === 'error') {
        // ✅ alles zurückrollen -> wieder sichtbar
        restoreRow(key)

        // ✅ Swipe zurück (nur Cards relevant, schadet sonst aber nicht)
        swipeRefs.current.get(key)?.reset()
        return
      }

      if (detail.phase === 'success') {
        markDeleting(key, false)
        return
      }
    }

    window.addEventListener('finished-downloads:delete', onExternalDelete as EventListener)
    return () => window.removeEventListener('finished-downloads:delete', onExternalDelete as EventListener)
  }, [animateRemove, markDeleting, queueRefill, restoreRow])

  useEffect(() => {
    const onReload = () => {
      if (refillInFlightRef.current) {
        if (!refillQueuedWhileInFlightRef.current) {
          refillQueuedWhileInFlightRef.current = true
        }
        return
      }

      if (refillTimerRef.current != null) return

      queueRefill()
    }

    window.addEventListener('finished-downloads:reload', onReload as EventListener)
    return () => window.removeEventListener('finished-downloads:reload', onReload as EventListener)
  }, [queueRefill])

  useEffect(() => {
    const onExternalRename = (ev: Event) => {
      const detail = (ev as CustomEvent<{ oldFile?: string; newFile?: string }>).detail
      const oldFile = String(detail?.oldFile ?? '').trim()
      const newFile = String(detail?.newFile ?? '').trim()
      if (!oldFile || !newFile || oldFile === newFile) return

      // ✅ nutzt eure bestehende Logik inkl. Aufräumen + durations-move
      applyRename(oldFile, newFile)
    }

    window.addEventListener('finished-downloads:rename', onExternalRename as EventListener)
    return () => window.removeEventListener('finished-downloads:rename', onExternalRename as EventListener)
  }, [applyRename])

  const visibleRows = useMemo(() => {
    const base = viewRows.filter((j) => !deletedKeys.has(keyFor(j)))

    // 🔎 Suche (AND über Tokens)
    const searched = searchTokens.length
      ? base.filter((j) => {
          const file = baseName(j.output || '')
          const model = modelNameFromOutput(j.output)
          const modelKey = lower(model)
          const tags = modelTags.tagsByModelKey[modelKey] ?? []

          const hay = lower([file, stripHotPrefix(file), model, j.id, tags.join(' ')].join(' '))

          for (const t of searchTokens) {
            if (!hay.includes(t)) return false
          }
          return true
        })
      : base

    if (activeTagSet.size === 0) return searched

    // AND-Filter: alle ausgewählten Tags müssen vorhanden sein
    return searched.filter((j) => {
      const modelKey = lower(modelNameFromOutput(j.output))

      const have = modelTags.tagSetByModelKey[modelKey]
      if (!have || have.size === 0) return false

      for (const t of activeTagSet) {
        if (!have.has(t)) return false
      }
      return true
    })
  }, [viewRows, deletedKeys, activeTagSet, modelTags, searchTokens])

  const totalItemsForPagination = effectiveAllMode ? visibleRows.length : doneTotalPage

  const pageRows = useMemo(() => {
    if (!effectiveAllMode) return visibleRows
    const start = (page - 1) * pageSize
    const end = start + pageSize
    return visibleRows.slice(Math.max(0, start), Math.max(0, end))
  }, [effectiveAllMode, visibleRows, page, pageSize])

  const emptyFolder = !effectiveAllMode && totalItemsForPagination === 0
  const emptyByFilter = globalFilterActive && visibleRows.length === 0

  // Optional zusätzlich: wenn allMode und wirklich gar nix da:
  const emptyAll = allMode && visibleRows.length === 0

  const showLoadingCard = isLoading && (emptyFolder || emptyAll || emptyByFilter)

  useEffect(() => {
    if (!globalFilterActive) return
    const totalPages = Math.max(1, Math.ceil(visibleRows.length / pageSize))
    if (page > totalPages) onPageChange(totalPages)
  }, [globalFilterActive, visibleRows.length, page, pageSize, onPageChange])

  // 🖱️ Desktop: Teaser nur bei Hover (Settings: 'hover' = Standard). Mobile: weiterhin Viewport-Fokus (Effect darunter)
  useEffect(() => {
    const active =
      teaserPlaybackMode === 'hover' &&
      !canHover &&
      (view === 'cards' || view === 'gallery' || view === 'table')

    if (!active) {
      setTeaserKey(null)
      teaserIORef.current?.disconnect()
      teaserIORef.current = null
      return
    }

    // Cards: Inline-Player soll “sticky” bleiben
    if (view === 'cards' && inlinePlay?.key) {
      setTeaserKey(inlinePlay.key)
      return
    }

    teaserIORef.current?.disconnect()

    const io = new IntersectionObserver(
      (entries) => {
        let bestKey: string | null = null
        let bestRatio = 0

        for (const ent of entries) {
          if (!ent.isIntersecting) continue
          const k = elToKeyRef.current.get(ent.target)
          if (!k) continue

          if (ent.intersectionRatio > bestRatio) {
            bestRatio = ent.intersectionRatio
            bestKey = k
          }
        }

        if (bestKey) {
          setTeaserKey((prev) => (prev === bestKey ? prev : bestKey))
        }
      },
      {
        // eher “welche Card ist wirklich sichtbar”
        threshold: [0, 0.15, 0.3, 0.5, 0.7, 0.9],
        rootMargin: '0px',
      }
    )

    teaserIORef.current = io

    for (const [k, el] of teaserHostsRef.current) {
      elToKeyRef.current.set(el, k)
      io.observe(el)
    }

    return () => {
      io.disconnect()
      if (teaserIORef.current === io) teaserIORef.current = null
    }
  }, [view, teaserPlaybackMode, canHover, inlinePlay?.key])

  // 🧠 Laufzeit-Anzeige: bevorzugt Videodauer, sonst Fallback auf startedAt/endedAt
  const runtimeOf = (job: RecordJob): string => {
    const k = keyFor(job)

    // ✅ 1) echte Videodauer (Preview-Metadaten oder Backend durationSeconds)
    const sec = durations[k] ?? (job as any)?.durationSeconds
    if (typeof sec === 'number' && Number.isFinite(sec) && sec > 0) {
      return formatDuration(sec * 1000)
    }

    // 2) Fallback
    const start = Date.parse(String(job.startedAt || ''))
    const end = Date.parse(String(job.endedAt || ''))
    if (Number.isFinite(start) && Number.isFinite(end) && end > start) {
      return formatDuration(end - start)
    }

    return '—'
  }

  const registerTeaserHost = useCallback(
    (key: string) => (el: HTMLDivElement | null) => {
      const prev = teaserHostsRef.current.get(key)
      if (prev && teaserIORef.current) teaserIORef.current.unobserve(prev)

      if (el) {
        teaserHostsRef.current.set(key, el)
        elToKeyRef.current.set(el, key)
        teaserIORef.current?.observe(el)
      } else {
        teaserHostsRef.current.delete(key)
      }
    },
    []
  )

  // Wird von FinishedVideoPreview aufgerufen, sobald die Metadaten da sind
  const handleDuration = useCallback((job: RecordJob, seconds: number) => {
    if (!Number.isFinite(seconds) || seconds <= 0) return
    const k = keyFor(job)

    const old = durationsRef.current[k]
    if (typeof old === 'number' && Math.abs(old - seconds) < 0.5) return

    durationsRef.current = { ...durationsRef.current, [k]: seconds }
    flushDurationsSoon()
  }, [flushDurationsSoon])

  const handleResolution = useCallback((job: RecordJob, w: number, h: number) => {
    if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return

    const k = keyFor(job)
    const prev = resolutionsRef.current[k]
    if (prev && prev.w === w && prev.h === h) return

    resolutionsRef.current = { ...resolutionsRef.current, [k]: { w, h } }
    flushResolutionsSoon()
  }, [flushResolutionsSoon])

  // ✅ Hooks immer zuerst – unabhängig von rows
  const isSmall = useMediaQuery('(max-width: 639px)')

  // ✅ Mobile-Offsets für Cards-Ansicht (zentral steuerbar)
  const cardsMobileOffsetTopClass = 'mt-10'
  const cardsMobileOffsetBottomClass = 'mb-2' // bei Bedarf z. B. 'mb-4'

  useEffect(() => {
    if (!isSmall || view !== 'cards') return

    const top = pageRows[0]
    if (!top) {
      setTeaserKey(null)
      return
    }

    const topKey = keyFor(top)

    // ✅ Erst mal kein sofortiger Teaser-Start auf der frisch promoted Card
    setTeaserKey((prev) => (prev === topKey ? prev : null))

    const t = window.setTimeout(() => {
      setTeaserKey((prev) => (prev === topKey ? prev : topKey))
    }, 140) // 100–180ms testen

    return () => window.clearTimeout(t)
  }, [isSmall, view, pageRows, keyFor])

  useEffect(() => {
    if (!isSmall) {
      // dein Cleanup (z.B. swipeRefs reset) wie gehabt
      swipeRefs.current = new Map()
    }
  }, [isSmall])

  useEffect(() => {
    // Nicht während eines Seitenwechsels / Refills zurückspringen.
    if (isLoading) return

    if (emptyFolder && page !== 1) onPageChange(1)
  }, [emptyFolder, isLoading, page, onPageChange])

  return (
    <>
      {/* Toolbar (sticky) */}
      <div className="sticky top-[56px] z-20">
        <div
          className="
            rounded-xl border border-gray-200/70 bg-white/80 shadow-sm
            backdrop-blur supports-[backdrop-filter]:bg-white/60
            dark:border-white/10 dark:bg-gray-950/60 dark:supports-[backdrop-filter]:bg-gray-950/40
          "
        >
          {/* Header row */}
          <div className="flex items-center gap-3 p-3">
            {/* Left: Title + Count */}
            <div className="hidden sm:flex items-center gap-2 min-w-0">
              <div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
                Abgeschlossene Downloads
              </div>
              <span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
                {totalItemsForPagination}
              </span>
            </div>

            {/* Mobile title (bleibt wie gehabt, aber kompakter) */}
            <div className="sm:hidden flex items-center gap-2 min-w-0 flex-1">
              <div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
                Abgeschlossene Downloads
              </div>
              <span className="shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-900 dark:bg-white/10 dark:text-gray-200">
                {totalItemsForPagination}
              </span>
            </div>

            {/* Right: Controls */}
            <div className="flex items-center gap-2 ml-auto shrink-0">
              {isLoading ? (
                <LoadingSpinner size="lg" className="text-indigo-500" srLabel="Lade Downloads…" />
              ) : null}
              {/* Desktop: Suche soll den Platz füllen */}
              <div className="hidden sm:flex items-center gap-2 min-w-0 flex-1">
                <input
                  value={searchQuery}
                  onChange={(e) => setSearchQuery(e.target.value)}
                  placeholder="Suchen…"
                  className="
                    h-9 w-full max-w-[420px] rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900 shadow-sm
                    focus:outline-none focus:ring-2 focus:ring-indigo-500
                    dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
                  "
                />
                {(searchQuery || '').trim() !== '' ? (
                  <Button size="sm" variant="soft" onClick={clearSearch}>
                    Leeren
                  </Button>
                ) : null}
              </div>

              {/* Desktop: Keep Toggle */}
              <div className="hidden sm:block">
                <LabeledSwitch
                  label="Behaltene Downloads anzeigen"
                  checked={includeKeep}
                  onChange={(checked) => {
                    if (page !== 1) onPageChange(1)
                    setIncludeKeep(checked)
                  }}
                />
              </div>

              {/* Desktop: Sort (nur wenn nicht Tabelle) */}
              {view !== 'table' && (
                <div className="hidden sm:block">
                  <label className="sr-only" htmlFor="finished-sort">
                    Sortierung
                  </label>
                  <select
                    id="finished-sort"
                    value={sortMode}
                    onChange={(e) => {
                      const m = e.target.value as SortMode
                      onSortModeChange(m)
                      if (page !== 1) onPageChange(1)
                    }}
                    className="
                      h-9 rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-900 shadow-sm
                      dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:[color-scheme:dark]
                    "
                  >
                    <option value="completed_desc">Fertiggestellt am ↓</option>
                    <option value="completed_asc">Fertiggestellt am ↑</option>
                    <option value="file_asc">Modelname A→Z</option>
                    <option value="file_desc">Modelname Z→A</option>
                    <option value="duration_desc">Dauer ↓</option>
                    <option value="duration_asc">Dauer ↑</option>
                    <option value="size_desc">Größe ↓</option>
                    <option value="size_asc">Größe ↑</option>
                  </select>
                </div>
              )}

              {/* Views */}

              <Button
                size='md'
                variant="soft"
                disabled={!lastAction || undoing}
                onClick={undoLastAction}
                title={
                  !lastAction
                    ? 'Keine Aktion zum Rückgängig machen'
                    : `Letzte Aktion rückgängig machen (${lastAction.kind})`
                }
              >
                Undo
              </Button>

              <ButtonGroup
                value={view}
                onChange={(id) => setView(id as ViewMode)}
                size={isSmall ? 'sm' : 'md'}
                ariaLabel="Ansicht"
                items={[
                  { id: 'table', icon: <TableCellsIcon className={isSmall ? 'size-4' : 'size-5'} />, label: isSmall ? undefined : 'Tabelle', srLabel: 'Tabelle' },
                  { id: 'cards', icon: <RectangleStackIcon className={isSmall ? 'size-4' : 'size-5'} />, label: isSmall ? undefined : 'Cards', srLabel: 'Cards' },
                  { id: 'gallery', icon: <Squares2X2Icon className={isSmall ? 'size-4' : 'size-5'} />, label: isSmall ? undefined : 'Galerie', srLabel: 'Galerie' },
                ]}
              />

              {/* Mobile: Optionen Button (unverändert) */}
              <button
                type="button"
                className="sm:hidden relative inline-flex items-center justify-center rounded-md border border-gray-200 bg-white p-2 shadow-sm
                          hover:bg-gray-50 dark:border-white/10 dark:bg-gray-900 dark:text-gray-100 dark:hover:bg-white/10"
                onClick={() => setMobileOptionsOpen((v) => !v)}
                aria-expanded={mobileOptionsOpen}
                aria-controls="finished-mobile-options"
                aria-label="Filter & Optionen"
              >
                <AdjustmentsHorizontalIcon className="size-5" />
                {/* kleiner Punkt wenn Filter aktiv */}
                {globalFilterActive || includeKeep ? (
                  <span className="absolute -top-1 -right-1 h-2.5 w-2.5 rounded-full bg-indigo-500 ring-2 ring-white dark:ring-gray-950" />
                ) : null}
              </button>
            </div>
          </div>

          {/* Desktop: aktive Tag-Filter anzeigen */}
          {tagFilter.length > 0 ? (
            <div className="hidden sm:flex items-center gap-2 border-t border-gray-200/60 dark:border-white/10 px-3 py-2">
              <span className="text-xs font-medium text-gray-600 dark:text-gray-300">
                Tag-Filter
              </span>

              <div className="flex flex-wrap items-center gap-1.5">
                {tagFilter.map((t) => (
                  <TagBadge key={t} tag={t} active={true} onClick={toggleTagFilter} />
                ))}

                <Button
                  className="
                    ml-1 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium
                    text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10
                  "
                  size="sm"
                  variant="soft"
                  onClick={clearTagFilter}
                >
                  Zurücksetzen
                </Button>
              </div>
            </div>
          ) : null}

          {/* Mobile Optionen (einklappbar): Suche + Keep + Sort */}
          <div
            id="finished-mobile-options"
            className={[
              'sm:hidden overflow-hidden transition-[max-height,opacity] duration-200 ease-in-out',
              mobileOptionsOpen ? 'max-h-[720px] opacity-100' : 'max-h-0 opacity-0',
            ].join(' ')}
          >
            {/* “Sheet”-Body */}
            <div className="border-t border-gray-200/60 dark:border-white/10 p-3">
              <div className="space-y-2">
                {/* Suche */}
                <div className="rounded-lg border border-gray-200/70 bg-white/70 p-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
                  <div className="flex items-center gap-2">
                    <input
                      value={searchQuery}
                      onChange={(e) => setSearchQuery(e.target.value)}
                      placeholder="Suchen…"
                      className="
                        h-10 w-full rounded-md border border-gray-200 bg-white px-3 text-sm text-gray-900
                        focus:outline-none focus:ring-2 focus:ring-indigo-500
                        dark:border-white/10 dark:bg-gray-950/60 dark:text-gray-100 dark:[color-scheme:dark]
                      "
                    />
                    {(searchQuery || '').trim() !== '' ? (
                      <Button size="sm" variant="soft" onClick={clearSearch}>
                        Clear
                      </Button>
                    ) : null}
                  </div>
                </div>

                {/* Keep – als Setting Row */}
                <div className="rounded-lg border border-gray-200/70 bg-white/70 px-3 py-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
                  <div className="flex items-center justify-between gap-3">
                    <div className="min-w-0">
                      <div className="text-sm font-medium text-gray-900 dark:text-gray-100">
                        Keep anzeigen
                      </div>
                      <div className="text-xs text-gray-600 dark:text-gray-300">
                        Behaltene Downloads in der Liste
                      </div>
                    </div>

                    <Switch
                      checked={includeKeep}
                      onChange={(checked) => {
                        if (page !== 1) onPageChange(1)
                        setIncludeKeep(checked)
                      }}
                      ariaLabel="Behaltene Downloads anzeigen"
                      size="default"
                    />
                  </div>
                </div>

                {/* Sort */}
                {view !== 'table' ? (
                  <div className="rounded-lg border border-gray-200/70 bg-white/70 px-3 py-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60">
                    <div className="text-xs font-medium text-gray-600 dark:text-gray-300 mb-1">
                      Sortierung
                    </div>
                    <select
                      id="finished-sort-mobile"
                      value={sortMode}
                      onChange={(e) => {
                        const m = e.target.value as SortMode
                        onSortModeChange(m)
                        if (page !== 1) onPageChange(1)
                      }}
                      className="
                        w-full h-10 rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-900 shadow-sm
                        dark:border-white/10 dark:bg-gray-950/60 dark:text-gray-100 dark:[color-scheme:dark]
                      "
                    >
                      <option value="completed_desc">Fertiggestellt am ↓</option>
                      <option value="completed_asc">Fertiggestellt am ↑</option>
                      <option value="file_asc">Modelname A→Z</option>
                      <option value="file_desc">Modelname Z→A</option>
                      <option value="duration_desc">Dauer ↓</option>
                      <option value="duration_asc">Dauer ↑</option>
                      <option value="size_desc">Größe ↓</option>
                      <option value="size_asc">Größe ↑</option>
                    </select>
                  </div>
                ) : null}
              </div>
            </div>

            {/* Tag-Filter */}
            {tagFilter.length > 0 ? (
              <div className="border-t border-gray-200/60 dark:border-white/10 px-3 py-2">
                <div className="flex flex-wrap items-center gap-2">
                  <span className="text-xs font-medium text-gray-600 dark:text-gray-300">Tag-Filter</span>

                  <div className="flex flex-wrap items-center gap-1.5">
                    {tagFilter.map((t) => (
                      <TagBadge key={t} tag={t} active={true} onClick={toggleTagFilter} />
                    ))}

                    <Button
                      className="
                        ml-1 inline-flex items-center rounded-md px-2 py-1 text-xs font-medium
                        text-gray-700 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-white/10
                      "
                      size="sm"
                      variant="soft"
                      onClick={clearTagFilter}
                    >
                      Zurücksetzen
                    </Button>
                  </div>
                </div>
              </div>
            ) : null}
          </div>
        </div>
      </div>

      {showLoadingCard ? (
        <Card grayBody>
          <div className="flex items-center justify-between gap-3">
            <div className="min-w-0">
              <div className="text-sm font-medium text-gray-900 dark:text-white">
                Lade Downloads…
              </div>
              <div className="text-xs text-gray-600 dark:text-gray-300">
                Bitte warte einen Moment.
              </div>
            </div>

            <LoadingSpinner
              size="lg"
              className="text-indigo-500"
              srLabel="Lade Downloads…"
            />
          </div>
        </Card>
      ) : (emptyFolder || emptyAll) ? (
        <Card grayBody>
          <div className="flex items-center gap-3">
            <div className="grid h-10 w-10 place-items-center rounded-xl bg-white/70 ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">
              <span className="text-lg">📁</span>
            </div>
            <div className="min-w-0">
              <div className="text-sm font-medium text-gray-900 dark:text-white">
                Keine abgeschlossenen Downloads
              </div>
              <div className="text-xs text-gray-600 dark:text-gray-300">
                Im Zielordner ist aktuell nichts vorhanden.
              </div>
            </div>
          </div>
        </Card>
      ) : emptyByFilter ? (
        <Card grayBody>
          <div className="text-sm text-gray-600 dark:text-gray-300">
            Keine Treffer für die aktuellen Filter.
          </div>

          {tagFilter.length > 0 || (searchQuery || '').trim() !== '' ? (
            <div className="mt-2 flex flex-wrap gap-3">
              {tagFilter.length > 0 ? (
                <button
                  type="button"
                  className="text-sm font-medium text-gray-700 hover:underline dark:text-gray-200"
                  onClick={clearTagFilter}
                >
                  Tag-Filter zurücksetzen
                </button>
              ) : null}

              {(searchQuery || '').trim() !== '' ? (
                <button
                  type="button"
                  className="text-sm font-medium text-gray-700 hover:underline dark:text-gray-200"
                  onClick={clearSearch}
                >
                  Suche zurücksetzen
                </button>
              ) : null}
            </div>
          ) : null}
        </Card>
      ) : (
        <>
          {view === 'cards' && (
            <div className={isSmall ? `${cardsMobileOffsetTopClass} ${cardsMobileOffsetBottomClass}` : ''}>
              <FinishedDownloadsCardsView
                rows={pageRows}
                isSmall={isSmall}
                isLoading={isLoading}
                blurPreviews={blurPreviews}
                durations={durations}
                teaserKey={teaserKey}
                teaserPlayback={teaserPlaybackMode}
                teaserAudio={teaserAudio}
                hoverTeaserKey={hoverTeaserKey}
                inlinePlay={inlinePlay}
                deletingKeys={deletingKeys}
                keepingKeys={keepingKeys}
                removingKeys={removingKeys}
                swipeRefs={swipeRefs}
                keyFor={keyFor}
                baseName={baseName}
                modelNameFromOutput={modelNameFromOutput}
                runtimeOf={runtimeOf}
                sizeBytesOf={sizeBytesOf}
                formatBytes={formatBytes}
                lower={lower}
                onOpenPlayer={onOpenPlayer}
                openPlayer={openPlayer}
                onOpenPlayerAt={openPlayerAt}
                handleScrubberClickIndex={handleScrubberClickIndex}
                startInlineAt={startInlineAt}
                startInline={startInline}
                tryAutoplayInline={tryAutoplayInline}
                registerTeaserHost={registerTeaserHost}
                handleDuration={handleDuration}
                deleteVideo={deleteVideo}
                keepVideo={keepVideo}
                releasePlayingFile={releasePlayingFile}
                modelsByKey={modelsByKey}
                onToggleHot={toggleHotVideo}
                onToggleFavorite={onToggleFavorite}
                onToggleLike={onToggleLike}
                onToggleWatch={onToggleWatch}
                onSplit={onSplitJob}
                activeTagSet={activeTagSet}
                onToggleTagFilter={toggleTagFilter}
                onHoverPreviewKeyChange={setHoverTeaserKey}
                assetNonce={assetNonce ?? 0}
                enqueueDeleteVideo={enqueueDeleteVideo}
                enqueueKeepVideo={enqueueKeepVideo}
                enqueueToggleHot={enqueueToggleHotVideo}
              />
            </div>
          )}

          {view === 'table' && (
            <FinishedDownloadsTableView
              rows={pageRows}
              isLoading={isLoading}
              keyFor={keyFor}
              baseName={baseName}
              lower={lower}
              modelNameFromOutput={modelNameFromOutput}
              runtimeOf={runtimeOf}
              sizeBytesOf={sizeBytesOf}
              formatBytes={formatBytes}
              resolutions={resolutions}
              durations={durations}
              canHover={canHover}
              teaserAudio={teaserAudio}
              hoverTeaserKey={hoverTeaserKey}
              setHoverTeaserKey={setHoverTeaserKey}
              teaserPlayback={teaserPlaybackMode}
              teaserKey={teaserKey}
              registerTeaserHost={registerTeaserHost}
              handleDuration={handleDuration}
              handleResolution={handleResolution}
              blurPreviews={blurPreviews}
              assetNonce={assetNonce}
              deletingKeys={deletingKeys}
              keepingKeys={keepingKeys}
              removingKeys={removingKeys}
              modelsByKey={modelsByKey}
              activeTagSet={activeTagSet}
              onToggleTagFilter={toggleTagFilter}
              onOpenPlayer={onOpenPlayer}
              handleScrubberClickIndex={handleScrubberClickIndex}
              onSortModeChange={onSortModeChange}
              page={page}
              onPageChange={onPageChange}
              onToggleHot={toggleHotVideo}
              onToggleFavorite={onToggleFavorite}
              onToggleLike={onToggleLike}
              onToggleWatch={onToggleWatch}
              onSplit={onSplitJob}
              deleteVideo={deleteVideo}
              keepVideo={keepVideo}
              enqueueDeleteVideo={enqueueDeleteVideo}
              enqueueKeepVideo={enqueueKeepVideo}
              enqueueToggleHot={enqueueToggleHotVideo}
            />
          )}

          {view === 'gallery' && (
            <FinishedDownloadsGalleryView
              rows={pageRows}
              isLoading={isLoading}
              blurPreviews={blurPreviews}
              durations={durations}
              handleDuration={handleDuration}
              teaserKey={teaserKey}
              teaserPlayback={teaserPlaybackMode}
              teaserAudio={teaserAudio}
              hoverTeaserKey={hoverTeaserKey}
              keyFor={keyFor}
              baseName={baseName}
              modelNameFromOutput={modelNameFromOutput}
              runtimeOf={runtimeOf}
              sizeBytesOf={sizeBytesOf}
              formatBytes={formatBytes}
              deletingKeys={deletingKeys}
              keepingKeys={keepingKeys}
              removingKeys={removingKeys}
              deletedKeys={deletedKeys}
              registerTeaserHost={registerTeaserHost}
              onOpenPlayer={onOpenPlayer}
              handleScrubberClickIndex={handleScrubberClickIndex}
              deleteVideo={deleteVideo}
              keepVideo={keepVideo}
              onToggleHot={toggleHotVideo}
              lower={lower}
              modelsByKey={modelsByKey}
              onToggleFavorite={onToggleFavorite}
              onToggleLike={onToggleLike}
              onToggleWatch={onToggleWatch}
              onSplit={onSplitJob}
              activeTagSet={activeTagSet}
              onToggleTagFilter={toggleTagFilter}
              onHoverPreviewKeyChange={setHoverTeaserKey}
              enqueueDeleteVideo={enqueueDeleteVideo}
              enqueueKeepVideo={enqueueKeepVideo}
              enqueueToggleHot={enqueueToggleHotVideo}
            />
          )}

          <Pagination
            page={page}
            pageSize={pageSize}
            totalItems={totalItemsForPagination}
            onPageChange={(p) => {
              flushSync(() => {
                setInlinePlay(null)
                setTeaserKey(null)
                setHoverTeaserKey(null)
              })

              for (const j of pageRows) {
                const f = baseName(j.output || '')
                if (!f) continue
                window.dispatchEvent(new CustomEvent('player:release', { detail: { file: f } }))
                window.dispatchEvent(new CustomEvent('player:close', { detail: { file: f } }))
              }

              window.scrollTo({ top: 0, behavior: 'auto' })
              onPageChange(p)
            }}
            showSummary={false}
            prevLabel="Zurück"
            nextLabel="Weiter"
            className="mt-4"
          />
        </>
      )}
    </>
  )

}
