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

'use client'

import {
  useEffect,
  useRef,
  useState,
  useMemo,
  useLayoutEffect,
  Fragment,
  type ReactNode,
  type MouseEvent as ReactMouseEvent,
} from 'react'
import type { RecordJob } from '../../types'
import {
  FireIcon as FireOutlineIcon,
  TrashIcon,
  BookmarkSquareIcon,
  MagnifyingGlassIcon,
  EllipsisVerticalIcon,
  StarIcon as StarOutlineIcon,
  HeartIcon as HeartOutlineIcon,
  EyeIcon as EyeOutlineIcon,
  ArrowDownTrayIcon,
  ScissorsIcon,
  AcademicCapIcon,
} from '@heroicons/react/24/outline'
import {
  FireIcon as FireSolidIcon,
  StarIcon as StarSolidIcon,
  HeartIcon as HeartSolidIcon,
  EyeIcon as EyeSolidIcon,
  CheckIcon,
} from '@heroicons/react/24/solid'
import { createPortal } from 'react-dom'


type Variant = 'overlay' | 'table'

type ActionKey =
  | 'details'
  | 'add'
  | 'training'
  | 'split'
  | 'hot'
  | 'favorite'
  | 'like'
  | 'watch'
  | 'keep'
  | 'delete'

type ActionResult = void | boolean
type ActionFn = (job: RecordJob) => ActionResult | Promise<ActionResult>

type Props = {
    job: RecordJob
    variant?: Variant
    collapseToMenu?: boolean
    inlineCount?: number

    busy?: boolean
    compact?: boolean

    isHot?: boolean
    isFavorite?: boolean
    isLiked?: boolean
    isWatching?: boolean

    onToggleFavorite?: ActionFn
    onToggleLike?: ActionFn
    onToggleHot?: ActionFn
    onKeep?: ActionFn
    onDelete?: ActionFn
    onToggleWatch?: ActionFn
    onAddToDownloads?: ActionFn
    onSplit?: ActionFn
    onAddToTraining?: ActionFn

    order?: ActionKey[]

    className?: string
}

function cn(...parts: Array<string | false | null | undefined>) {
  return parts.filter(Boolean).join(' ')
}

const baseName = (p: string) =>
  (p || '').replaceAll('\\', '/').trim().split('/').pop() || ''

const stripHotPrefix = (name: string) => (name.startsWith('HOT ') ? name.slice(4) : name)

// wie backend models.go / App.tsx
const reModel = /^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}/

function modelKeyFromOutput(output?: string): string | null {
  const file = stripHotPrefix(baseName(output || ''))
  const base = file.replace(/\.[^.]+$/, '') // ext weg

  const m = base.match(reModel)
  if (m?.[1]?.trim()) return m[1].trim()

  const i = base.lastIndexOf('_')
  if (i > 0) return base.slice(0, i)

  return base ? base : null
}

export default function RecordJobActions({
  job,
  variant = 'overlay',
  collapseToMenu = false,
  inlineCount,
  busy = false,
  compact = false,
  isHot = false,
  isFavorite = false,
  isLiked = false,
  isWatching = false,
  onToggleFavorite,
  onToggleLike,
  onToggleHot,
  onKeep,
  onDelete,
  onToggleWatch,
  onAddToDownloads,
  onSplit,
  onAddToTraining,
  order,
  className,
}: Props) {
  
  const pad = variant === 'overlay' ? (compact ? 'p-1.5' : 'p-2') : 'p-1.5'

  const iconBox = compact ? 'size-4' : 'size-5'
  const iconFill = 'h-full w-full'

  const btnBase =
    variant === 'table'
      ? [
          'inline-flex items-center justify-center rounded-md',
          pad,
          'bg-gray-50/90 text-gray-700 shadow-sm ring-1 ring-gray-200/80',
          'hover:bg-white hover:text-gray-950 hover:ring-gray-300',
          'dark:bg-white/5 dark:text-gray-200 dark:ring-white/10 dark:hover:bg-white/10 dark:hover:text-white',
          'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60',
          'disabled:cursor-not-allowed disabled:opacity-50 transition-transform active:scale-95',
        ].join(' ')
      : 'inline-flex items-center justify-center rounded-md ' +
        `bg-white/75 ${pad} text-gray-900 backdrop-blur ring-1 ring-black/10 ` +
        'hover:bg-white/90 ' +
        'dark:bg-black/40 dark:text-white dark:ring-white/10 dark:hover:bg-black/60 ' +
        'focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/60 dark:focus-visible:ring-white/40 ' +
        'disabled:opacity-50 disabled:cursor-not-allowed transition-transform active:scale-95'

  const colors =
    variant === 'table'
      ? {
          favOn: 'text-amber-600 dark:text-amber-300',
          likeOn: 'text-rose-600 dark:text-rose-300',
          hotOn: 'text-amber-600 dark:text-amber-300',
          off: 'text-gray-800 dark:text-gray-100',
          keep: 'text-emerald-600 dark:text-emerald-300',
          del: 'text-red-600 dark:text-red-300',
          watchOn: 'text-sky-600 dark:text-sky-300',
        }
      : {
          favOn: 'text-amber-600 dark:text-amber-300',
          likeOn: 'text-rose-600 dark:text-rose-300',
          hotOn: 'text-amber-600 dark:text-amber-300',
          off: 'text-gray-800/90 dark:text-white/90',
          keep: 'text-emerald-600 dark:text-emerald-200',
          del: 'text-red-600 dark:text-red-300',
          watchOn: 'text-sky-600 dark:text-sky-200',
        }


  // ✅ Reihenfolge strikt nach `order` (wenn gesetzt). Keys die nicht im order stehen: niemals anzeigen.
  const outlineStrokeWidth = variant === 'table' ? 1.75 : undefined

  const actionOrder: ActionKey[] = order ?? ['watch', 'favorite', 'like', 'training', 'split', 'hot', 'keep', 'delete', 'details']
  const inOrder = (k: ActionKey) => actionOrder.includes(k)

  const addUrl = String((job as any)?.sourceUrl ?? '').trim()
  const detailsKey = modelKeyFromOutput(job.output || '')
  const detailsLabel = detailsKey ? `Mehr zu ${detailsKey} anzeigen` : 'Mehr anzeigen'

  // Sichtbarkeit NUR über `order`
  const wantFavorite = inOrder('favorite')
  const wantLike = inOrder('like')
  const wantHot = inOrder('hot')
  const wantWatch = inOrder('watch')
  const wantKeep = inOrder('keep')
  const wantDelete = inOrder('delete')
  const wantSplit = inOrder('split') && Boolean(baseName(job.output || ''))
  const wantTraining = inOrder('training') && Boolean(baseName(job.output || ''))

  // Details: wenn du ihn auch ohne detailsKey zeigen willst, nimm nur inOrder('details').
  // (Aktuell macht Details ohne detailsKey wenig Sinn, daher: nur anzeigen wenn key existiert.)
  const wantDetails = inOrder('details') && Boolean(detailsKey)

  // Add: nur über order steuern (disabled wenn keine URL/Handler)
  const wantAdd = inOrder('add')

  const [addState, setAddState] = useState<'idle' | 'busy' | 'ok'>('idle')
  const addTimerRef = useRef<number | null>(null)

  useEffect(() => {
    return () => {
      if (addTimerRef.current) window.clearTimeout(addTimerRef.current)
    }
  }, [])

  const flashAddOk = () => {
    setAddState('ok')
    if (addTimerRef.current) window.clearTimeout(addTimerRef.current)
    addTimerRef.current = window.setTimeout(() => setAddState('idle'), 800)
  }

  const doAdd = async (): Promise<boolean> => {
    if (busy) return false
    if (addState === 'busy') return false

    const hasUrl = Boolean(addUrl)
    if (!onAddToDownloads && !hasUrl) return false

    setAddState('busy')
    try {
      let ok = true

      if (onAddToDownloads) {
        const r = await onAddToDownloads(job)
        ok = r !== false
      } else if (addUrl) {
        window.dispatchEvent(new CustomEvent('downloads:add-url', { detail: { url: addUrl } }))
        ok = true
      } else {
        ok = false
      }

      if (ok) flashAddOk()
      else setAddState('idle')

      return ok
    } catch {
      setAddState('idle')
      return false
    }
  }

  const doTrainingImport = async (): Promise<boolean> => {
    if (busy) return false

    const output = String(job.output || '').trim()
    if (!output) return false

    try {
      if (onAddToTraining) {
        const r = await onAddToTraining(job)
        return r !== false
      }

      const detail = {
        jobId: String((job as any)?.id ?? ''),
        output,
        sourceFile: baseName(output),
        count: 10,
      }

      // Falls TrainingTab gerade nicht gemountet ist.
      try {
        window.sessionStorage.setItem(
          'training:pending-import-video',
          JSON.stringify(detail)
        )
      } catch {
        // ignore
      }

      // App kann darauf reagieren und zum Training-Tab wechseln.
      window.dispatchEvent(
        new CustomEvent('app:navigate-tab', {
          detail: { tab: 'training' },
        })
      )

      // Einen Frame warten, damit der TrainingTab nach dem Tab-Wechsel sicher gemountet/aktiv ist.
      // sessionStorage bleibt trotzdem der Fallback.
      window.requestAnimationFrame(() => {
        window.dispatchEvent(
          new CustomEvent('training:import-video', {
            detail,
          })
        )
      })

      return true
    } catch {
      return false
    }
  }

  // ✅ Auto-Fit: verfügbare Breite + tatsächlicher gap (Tailwind gap-1/gap-2/…)
  const [rootW, setRootW] = useState(0)
  const [gapPx, setGapPx] = useState(4)

  const run = (fn?: ActionFn) => (e: ReactMouseEvent<HTMLButtonElement>) => {
    e.preventDefault()
    e.stopPropagation()
    if (busy || !fn) return
    void Promise.resolve(fn(job)).catch(() => {})
  }

  const DetailsBtn =
    wantDetails ? (
      <button
        type="button"
        className={cn(btnBase)}
        title={detailsLabel}
        aria-label={detailsLabel}
        disabled={busy}
        onClick={(e) => {
          e.preventDefault()
          e.stopPropagation()
          if (busy) return
          window.dispatchEvent(new CustomEvent('open-model-details', { detail: { modelKey: detailsKey } }))
        }}
      >
        <span className={cn('inline-flex items-center justify-center', iconBox)}>
          <MagnifyingGlassIcon className={cn(iconFill, colors.off)} strokeWidth={outlineStrokeWidth} />
        </span>
        <span className="sr-only">{detailsLabel}</span>
      </button>
    ) : null

  const AddBtn = wantAdd ? (
    <button
      type="button"
      className={cn(btnBase)}
      title={addUrl ? 'URL zu Downloads hinzufügen' : 'Keine URL vorhanden'}
      aria-label="Zu Downloads hinzufügen"
      disabled={busy || addState === 'busy' || (!onAddToDownloads && !addUrl)}
      onClick={async (e) => {
        e.preventDefault()
        e.stopPropagation()
        await doAdd()
      }}
    >
      <span className={cn('inline-flex items-center justify-center', iconBox)}>
        {addState === 'ok' ? (
          <CheckIcon className={cn(iconFill, 'text-emerald-600 dark:text-emerald-300')} />
        ) : (
          <ArrowDownTrayIcon className={cn(iconFill, colors.off)} strokeWidth={outlineStrokeWidth} />
        )}
      </span>
      <span className="sr-only">Zu Downloads</span>
    </button>
  ) : null

  const TrainingBtn = wantTraining ? (
    <button
      type="button"
      className={btnBase}
      title="Video ins Training übernehmen"
      aria-label="Video ins Training übernehmen"
      disabled={busy || (!onAddToTraining && !job.output)}
      onClick={async (e) => {
        e.preventDefault()
        e.stopPropagation()
        await doTrainingImport()
      }}
    >
      <span className={cn('inline-flex items-center justify-center', iconBox)}>
        <AcademicCapIcon className={cn(iconFill, colors.off)} strokeWidth={outlineStrokeWidth} />
      </span>
      <span className="sr-only">Ins Training übernehmen</span>
    </button>
  ) : null

  const FavoriteBtn = wantFavorite ? (
    <button
      type="button"
      className={btnBase}
      title={isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
      aria-label={isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
      aria-pressed={isFavorite}
      disabled={busy || !onToggleFavorite}
      onClick={run(onToggleFavorite)}
    >
      <span className={cn('relative inline-block', iconBox)}>
        <StarOutlineIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out motion-reduce:transition-none',
            isFavorite ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
            iconFill,
            colors.off
          )}
          strokeWidth={outlineStrokeWidth}
        />
        <StarSolidIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out motion-reduce:transition-none',
            isFavorite ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
            iconFill,
            colors.favOn
          )}
        />
      </span>
    </button>
  ) : null

  const LikeBtn = wantLike ? (
    <button
      type="button"
      className={btnBase}
      title={isLiked ? 'Gefällt mir entfernen' : 'Als Gefällt mir markieren'}
      aria-label={isLiked ? 'Gefällt mir entfernen' : 'Als Gefällt mir markieren'}
      aria-pressed={isLiked}
      disabled={busy || !onToggleLike}
      onClick={run(onToggleLike)}
    >
      <span className={cn('relative inline-block', iconBox)}>
        <HeartOutlineIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out motion-reduce:transition-none',
            isLiked ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
            iconFill,
            colors.off
          )}
          strokeWidth={outlineStrokeWidth}
        />
        <HeartSolidIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out motion-reduce:transition-none',
            isLiked ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
            iconFill,
            colors.likeOn
          )}
        />
      </span>
    </button>
  ) : null

  const HotBtn = wantHot ? (
    <button
      type="button"
      data-hot-target
      className={btnBase}
      title={isHot ? 'HOT entfernen' : 'Als HOT markieren'}
      aria-label={isHot ? 'HOT entfernen' : 'Als HOT markieren'}
      aria-pressed={isHot}
      disabled={busy || !onToggleHot}
      onClick={run(onToggleHot)}
    >
      <span className={cn('relative inline-block', iconBox)}>
        <FireOutlineIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out',
            isHot ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
            iconFill,
            colors.off
          )}
          strokeWidth={outlineStrokeWidth}
        />
        <FireSolidIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out',
            isHot ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
            iconFill,
            colors.hotOn
          )}
        />
      </span>
    </button>
  ) : null

  const WatchBtn = wantWatch ? (
    <button
      type="button"
      className={btnBase}
      title={isWatching ? 'Watched entfernen' : 'Als Watched markieren'}
      aria-label={isWatching ? 'Watched entfernen' : 'Als Watched markieren'}
      aria-pressed={isWatching}
      disabled={busy || !onToggleWatch}
      onClick={run(onToggleWatch)}
    >
      <span className={cn('relative inline-block', iconBox)}>
        <EyeOutlineIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out',
            isWatching ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
            iconFill,
            colors.off
          )}
          strokeWidth={outlineStrokeWidth}
        />
        <EyeSolidIcon
          className={cn(
            'absolute inset-0 transition-all duration-200 ease-out',
            isWatching ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
            iconFill,
            colors.watchOn
          )}
        />
      </span>
    </button>
  ) : null

  const SplitBtn = wantSplit ? (
    <button
      type="button"
      className={btnBase}
      title="Video schneiden"
      aria-label="Video schneiden"
      disabled={busy || !onSplit}
      onClick={run(onSplit)}
    >
      <span className={cn('inline-flex items-center justify-center', iconBox)}>
        <ScissorsIcon className={cn(iconFill, colors.off)} strokeWidth={outlineStrokeWidth} />
      </span>
    </button>
  ) : null

  const KeepBtn = wantKeep ? (
    <button
      type="button"
      className={btnBase}
      title="Behalten (nach keep verschieben)"
      aria-label="Behalten"
      disabled={busy || !onKeep}
      onClick={run(onKeep)}
    >
      <span className={cn('inline-flex items-center justify-center', iconBox)}>
        <BookmarkSquareIcon className={cn(iconFill, colors.keep)} strokeWidth={outlineStrokeWidth} />
      </span>
    </button>
  ) : null

  const DeleteBtn = wantDelete ? (
    <button
      type="button"
      className={btnBase}
      title="Löschen"
      aria-label="Löschen"
      disabled={busy || !onDelete}
      onClick={run(onDelete)}
    >
      <span className={cn('inline-flex items-center justify-center', iconBox)}>
        <TrashIcon className={cn(iconFill, colors.del)} strokeWidth={outlineStrokeWidth} />
      </span>
    </button>
  ) : null
  
  const byKey: Record<ActionKey, ReactNode> = {
    details: DetailsBtn,
    add: AddBtn,
    training: TrainingBtn,
    split: SplitBtn,
    favorite: FavoriteBtn,
    like: LikeBtn,
    watch: WatchBtn,
    hot: HotBtn,
    keep: KeepBtn,
    delete: DeleteBtn,
  }

  const collapse = collapseToMenu

  // Nur Keys, die wirklich einen Node haben
  const presentKeys = actionOrder.filter((k) => Boolean(byKey[k]))

  // ✅ Wenn inlineCount NICHT gesetzt ist -> Auto-Fit (so viele wie passen + Menü-Icon)
  const autoFit = collapse && typeof inlineCount !== 'number'

  // grobe, aber sehr stabile Button-Breite in px (Icon + Padding links/rechts)
  const padPx = variant === 'overlay' ? (compact ? 6 : 8) : 6 // p-1.5=6px, p-2=8px
  const iconPx = compact ? 16 : 20 // size-4 / size-5
  const btnW = iconPx + padPx * 2

  const fallbackLimit = compact ? 2 : 3

  const fitLimit = useMemo(() => {
    if (!autoFit) return (inlineCount ?? fallbackLimit)

    // rootW kann initial 0 sein -> brauchbarer Fallback
    const w = rootW || 0
    if (w <= 0) return Math.min(presentKeys.length, fallbackLimit)

    // Wir reservieren IMMER Platz für das Menü-Icon (btnW).
    // Gaps: zwischen den inline Buttons + zwischen letztem inline und Menü => inlineCount Gaps.
    for (let i = presentKeys.length; i >= 0; i--) {
      const total = i * btnW + btnW + (i > 0 ? i * gapPx : 0)
      if (total <= w) return i
    }
    return 0
  }, [autoFit, inlineCount, fallbackLimit, rootW, presentKeys.length, btnW, gapPx])

  const inlineKeys = collapse ? presentKeys.slice(0, fitLimit) : presentKeys
  const menuKeys = collapse ? presentKeys.slice(fitLimit) : []

  const [menuOpen, setMenuOpen] = useState(false)
  const menuRef = useRef<HTMLDivElement | null>(null)

  useLayoutEffect(() => {
    const el = menuRef.current
    if (!el || typeof window === 'undefined') return

    const read = () => {
      const self = menuRef.current
      if (!self) return

      // ✅ WICHTIG: verfügbare Breite = eigene Breite (so stimmt es auch mit Geschwistern wie "Stop")
      const w = Math.floor(self.getBoundingClientRect().width || 0)
      if (w > 0) setRootW(w)

      const cs = window.getComputedStyle(self)
      const gRaw = (cs.columnGap || (cs as any).gap || '0') as string
      const g = parseFloat(gRaw)
      if (Number.isFinite(g) && g >= 0) setGapPx(g)
    }

    read()

    const ro = new ResizeObserver(() => read())
    ro.observe(el)

    window.addEventListener('resize', read)
    return () => {
      window.removeEventListener('resize', read)
      ro.disconnect()
    }
  }, [])

  // ✅ neu
  const menuBtnRef = useRef<HTMLButtonElement | null>(null)
  const menuPopupRef = useRef<HTMLDivElement | null>(null)

  const MENU_W = 208 // entspricht Tailwind w-52 (13rem)
  const GAP = 4
  const MARGIN = 8

  const [menuStyle, setMenuStyle] = useState<{ top: number; left: number; maxH: number } | null>(null)
  const [menuPortalTarget, setMenuPortalTarget] = useState<HTMLElement | null>(null)

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

    const onKey = (e: KeyboardEvent) => {
      if (e.key === 'Escape') setMenuOpen(false)
    }

    const onDown = (e: MouseEvent) => {
      const root = menuRef.current
      const popup = menuPopupRef.current
      const target = e.target as Node

      // ✅ Click innerhalb der Action-Root ODER innerhalb des Portal-Menüs -> offen lassen
      if ((root && root.contains(target)) || (popup && popup.contains(target))) return
      setMenuOpen(false)
    }

    window.addEventListener('keydown', onKey)
    window.addEventListener('mousedown', onDown)
    return () => {
      window.removeEventListener('keydown', onKey)
      window.removeEventListener('mousedown', onDown)
    }
  }, [menuOpen])

  useLayoutEffect(() => {
    if (!menuOpen) {
      setMenuStyle(null)
      setMenuPortalTarget(null)
      return
    }

    const portalTarget =
      (menuBtnRef.current?.closest('dialog[open]') as HTMLElement | null) ??
      document.body

    setMenuPortalTarget(portalTarget)

    const update = () => {
      const btn = menuBtnRef.current
      if (!btn) return

      const r = btn.getBoundingClientRect()
      const vw = window.innerWidth
      const vh = window.innerHeight

      let left = r.right - MENU_W
      left = Math.max(MARGIN, Math.min(left, vw - MENU_W - MARGIN))

      let top = r.bottom + GAP

      const spaceBelow = vh - top - MARGIN
      const spaceAbove = r.top - MARGIN

      // Falls unten zu wenig Platz ist, über dem Button öffnen
      if (spaceBelow < 120 && spaceAbove > spaceBelow) {
        const maxH = Math.max(120, spaceAbove)
        top = Math.max(MARGIN, r.top - GAP - Math.min(maxH, 320))
        setMenuStyle({ top, left, maxH })
        return
      }

      top = Math.max(MARGIN, Math.min(top, vh - MARGIN))
      const maxH = Math.max(120, vh - top - MARGIN)

      setMenuStyle({ top, left, maxH })
    }

    update()

    window.addEventListener('resize', update)
    window.addEventListener('scroll', update, true)

    return () => {
      window.removeEventListener('resize', update)
      window.removeEventListener('scroll', update, true)
    }
  }, [menuOpen])

  const fireDetails = () => {
    if (!detailsKey) return
    window.dispatchEvent(new CustomEvent('open-model-details', { detail: { modelKey: detailsKey } }))
  }

  const menuItem = (k: ActionKey) => {
    if (k === 'details') {
      return (
        <button
          key="details"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy}
          onClick={(e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            fireDetails()
          }}
        >
          <MagnifyingGlassIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          <span className="truncate">Details</span>
        </button>
      )
    }
    
    if (k === 'add') {
      return (
        <button
          key="add"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || (!onAddToDownloads && !addUrl)}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await doAdd()
          }}
        >
          <ArrowDownTrayIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          <span className="truncate">Zu Downloads</span>
        </button>
      )
    }

    if (k === 'training') {
      return (
        <button
          key="training"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || (!onAddToTraining && !job.output)}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await doTrainingImport()
          }}
        >
          <AcademicCapIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          <span className="truncate">Ins Training übernehmen</span>
        </button>
      )
    }

    if (k === 'split') {
      return (
        <button
          key="split"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onSplit}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await onSplit?.(job)
          }}
        >
          <ScissorsIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          <span className="truncate">Video schneiden</span>
        </button>
      )
    }

    if (k === 'favorite') {
      return (
        <button
          key="favorite"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onToggleFavorite}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await onToggleFavorite?.(job)
          }}
        >
          {isFavorite ? (
            <StarSolidIcon className={cn('size-4', colors.favOn)} />
          ) : (
            <StarOutlineIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          )}
          <span className="truncate">{isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}</span>
        </button>
      )
    }

    if (k === 'like') {
      return (
        <button
          key="like"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onToggleLike}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await onToggleLike?.(job)
          }}
        >
          {isLiked ? (
            <HeartSolidIcon className={cn('size-4', colors.favOn)} />
          ) : (
            <HeartOutlineIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          )}
          <span className="truncate">{isLiked ? 'Gefällt mir entfernen' : 'Gefällt mir'}</span>
        </button>
      )
    }

    if (k === 'watch') {
      return (
        <button
          key="watch"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onToggleWatch}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await onToggleWatch?.(job)
          }}
        >
          {isWatching ? (
            <EyeSolidIcon className={cn('size-4', colors.favOn)} />
          ) : (
            <EyeOutlineIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          )}
          <span className="truncate">{isWatching ? 'Watched entfernen' : 'Watched'}</span>
        </button>
      )
    }

    if (k === 'hot') {
      return (
        <button
          key="hot"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onToggleHot}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            await onToggleHot?.(job)
          }}
        >
          {isHot ? (
            <FireSolidIcon className={cn('size-4', colors.favOn)} />
          ) : (
            <FireOutlineIcon className={cn('size-4', colors.off)} strokeWidth={outlineStrokeWidth} />
          )}
          <span className="truncate">{isHot ? 'HOT entfernen' : 'Als HOT markieren'}</span>
        </button>
      )
    }

    if (k === 'keep') {
      return (
        <button
          key="keep"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onKeep}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            try {
              await onKeep?.(job)
            } catch {
              // The app-level handler already reports delete/keep failures.
            }
          }}
        >
          <BookmarkSquareIcon className={cn('size-4', colors.keep)} strokeWidth={outlineStrokeWidth} />
          <span className="truncate">Behalten</span>
        </button>
      )
    }

    if (k === 'delete') {
      return (
        <button
          key="delete"
          type="button"
          className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm hover:bg-gray-100/70 dark:hover:bg-white/5 disabled:opacity-50"
          disabled={busy || !onDelete}
          onClick={async (e) => {
            e.preventDefault()
            e.stopPropagation()
            setMenuOpen(false)
            try {
              await onDelete?.(job)
            } catch {
              // The app-level handler already reports delete/keep failures.
            }
          }}
        >
          <TrashIcon className={cn('size-4', colors.del)} strokeWidth={outlineStrokeWidth} />
          <span className="truncate">Löschen</span>
        </button>
      )
    }

    return null
  }

  return (
    <div ref={menuRef} className={cn('relative flex items-center flex-nowrap', className ?? 'gap-2')}>
      {inlineKeys.map((k) => {
        const node = byKey[k]
        return node ? <Fragment key={k}>{node}</Fragment> : null
      })}

      {collapse && menuKeys.length > 0 ? (
        <div className="relative">
          <button
            ref={menuBtnRef}
            type="button"
            className={btnBase}
            aria-label="Mehr"
            title="Mehr"
            aria-haspopup="menu"
            aria-expanded={menuOpen}
            disabled={busy}
            onClick={(e) => {
              e.preventDefault()
              e.stopPropagation()
              if (busy) return
              setMenuOpen((v) => !v)
            }}
          >
            <span className={cn('inline-flex items-center justify-center', iconBox)}>
              <EllipsisVerticalIcon className={cn(iconFill, colors.off)} strokeWidth={outlineStrokeWidth} />
            </span>
          </button>

          {menuOpen && menuStyle && menuPortalTarget
            ? createPortal(
                <div
                  ref={menuPopupRef}
                  role="menu"
                  className="overflow-auto rounded-md bg-white/95 p-1 text-gray-900 shadow-2xl ring-1 ring-black/10 backdrop-blur dark:bg-gray-900/95 dark:text-gray-100 dark:ring-white/10"
                  style={{
                    position: 'fixed',
                    top: menuStyle.top,
                    left: menuStyle.left,
                    width: MENU_W,
                    maxHeight: menuStyle.maxH,
                    zIndex: 2147483647,
                  }}
                  onClick={(e) => e.stopPropagation()}
                >
                  {menuKeys.map(menuItem)}
                </div>,
                menuPortalTarget
              )
            : null}
        </div>
      ) : null}
    </div>
  )
}
