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

'use client'

import { useEffect, useRef, useState, useMemo, useCallback, type MouseEvent, type PointerEvent, type CSSProperties } from 'react'
import type { RecordJob } from '../../types'
import Modal from './Modal'
import Button from './Button'
import Checkbox from './Checkbox'
import { LinkIcon } from '@heroicons/react/24/outline'
import {
  getSegmentLabelText,
  getHighestPrioritySegmentLabelItem,
} from './Icons'
import { formatDateTime } from './formatters'

type Segment = {
  index: number
  label: string
  start: number
  end: number
  duration: number
  markerTime?: number
}

type SplitApplyProgress = {
  phase: 'preparing' | 'splitting' | 'done' | 'error'
  total: number
  current: number
  completed: number
  segmentProgress?: number
  overallProgress?: number
  message?: string
  segment?: Pick<Segment, 'index' | 'label' | 'start' | 'end' | 'duration'>
}

type Props = {
  open: boolean
  onClose: () => void
  job: RecordJob | null
  videoSrc: string
  timelinePreviewCount?: number
  onApply?: (payload: {
    job: RecordJob
    splits: number[]
    segments: Segment[]
    deleteOriginal: boolean
    onProgress?: (progress: SplitApplyProgress) => void
  }) => void | Promise<void>
}

type PreviewSpriteMeta = {
  exists: boolean
  path?: string
  count?: number
  cols?: number
  rows?: number
  stepSeconds?: number
}

type TimelinePreviewTile = {
  index: number
  time: number
  label: string
}

type PreviewClipMap = {
  start: number
  dur: number
  cumStart: number
  cumEnd: number
}

function mapOptionalTime(
  value: unknown,
  timeMapper?: (timeSec: number) => number
): number | undefined {
  const n = Number(value)
  if (!Number.isFinite(n)) return undefined

  const mapped = timeMapper ? Number(timeMapper(n)) : n
  return Number.isFinite(mapped) ? mapped : undefined
}

function extractPreviewClipMap(data: any): PreviewClipMap[] | null {
  let raw =
    data?.meta?.preview?.clips ??
    data?.meta?.preview?.teaser?.clips ??
    data?.preview?.clips ??
    data?.preview?.teaser?.clips ??
    data?.previewClips ??
    data?.teaserClips ??
    null

  if (typeof raw === 'string') {
    try {
      raw = JSON.parse(raw)
    } catch {
      raw = null
    }
  }

  if (!Array.isArray(raw) || raw.length === 0) return null

  let cum = 0
  const out: PreviewClipMap[] = []

  for (const item of raw) {
    const start = Number(
      pickFirstDefined(
        item?.startSeconds,
        item?.StartSeconds,
        item?.start,
        item?.Start
      )
    )

    const dur = Number(
      pickFirstDefined(
        item?.durationSeconds,
        item?.DurationSeconds,
        item?.duration,
        item?.Duration
      )
    )

    if (!Number.isFinite(start) || start < 0) continue
    if (!Number.isFinite(dur) || dur <= 0) continue

    out.push({
      start,
      dur,
      cumStart: cum,
      cumEnd: cum + dur,
    })

    cum += dur
  }

  return out.length > 0 ? out : null
}

function mapPreviewTimeToVideoTime(
  timeSec: number,
  clipMap: PreviewClipMap[]
): number {
  if (!Number.isFinite(timeSec) || clipMap.length === 0) return timeSec

  if (timeSec <= 0) return clipMap[0].start

  const last = clipMap[clipMap.length - 1]
  if (timeSec >= last.cumEnd) {
    return last.start + last.dur
  }

  let lo = 0
  let hi = clipMap.length - 1

  while (lo <= hi) {
    const mid = (lo + hi) >> 1
    const clip = clipMap[mid]

    if (timeSec < clip.cumStart) {
      hi = mid - 1
    } else if (timeSec >= clip.cumEnd) {
      lo = mid + 1
    } else {
      return clip.start + (timeSec - clip.cumStart)
    }
  }

  const idx = clamp(lo, 0, clipMap.length - 1)
  return clipMap[idx].start
}

function maxAiRawTime(ai: any): number {
  if (!ai || typeof ai !== 'object') return 0

  let max = 0

  const collect = (v: unknown) => {
    const n = Number(v)
    if (Number.isFinite(n) && n > max) max = n
  }

  const hits = Array.isArray(ai?.hits ?? ai?.Hits) ? (ai?.hits ?? ai?.Hits) : []
  for (const item of hits) {
    collect(item?.time)
    collect(item?.Time)
    collect(item?.at)
    collect(item?.timestamp)
    collect(item?.start)
    collect(item?.Start)
    collect(item?.startSeconds)
    collect(item?.StartSeconds)
    collect(item?.end)
    collect(item?.End)
    collect(item?.endSeconds)
    collect(item?.EndSeconds)
  }

  const segments = Array.isArray(ai?.segments ?? ai?.Segments) ? (ai?.segments ?? ai?.Segments) : []
  for (const item of segments) {
    collect(item?.start)
    collect(item?.Start)
    collect(item?.startSeconds)
    collect(item?.StartSeconds)
    collect(item?.end)
    collect(item?.End)
    collect(item?.endSeconds)
    collect(item?.EndSeconds)
  }

  return max
}

function shouldMapAiPreviewTimes(
  ai: any,
  clipMap: PreviewClipMap[] | null,
  durationSec: number
): boolean {
  if (!clipMap || clipMap.length === 0) return false
  if (!Number.isFinite(durationSec) || durationSec <= 0) return false

  const previewDuration = clipMap[clipMap.length - 1]?.cumEnd ?? 0
  const aiMax = maxAiRawTime(ai)

  if (!Number.isFinite(previewDuration) || previewDuration <= 0) return false
  if (!Number.isFinite(aiMax) || aiMax <= 0) return false

  // Nur mappen, wenn AI-Zeiten klar auf die Preview-Zeitachse passen
  // und die Preview deutlich kürzer ist als das Vollvideo.
  return aiMax <= previewDuration * 1.05 && previewDuration < durationSec * 0.95
}

function frameIndexFromTime(
  timeSec: number,
  durationSec: number,
  meta: PreviewSpriteMeta
): number {
  const count = Math.max(1, Number(meta.count || 0))
  const stepSeconds = Number(meta.stepSeconds || 0)

  if (count <= 1) return 0

  const spriteCoverageSec =
    stepSeconds > 0
      ? stepSeconds * Math.max(1, count - 1)
      : 0

  const stepLooksUsable =
    stepSeconds > 0 &&
    durationSec > 0 &&
    spriteCoverageSec >= durationSec * 0.7 &&
    spriteCoverageSec <= durationSec * 1.3

  if (stepLooksUsable) {
    const idx = Math.round(timeSec / stepSeconds)
    return clamp(idx, 0, count - 1)
  }

  if (durationSec > 0) {
    const ratio = clamp(timeSec / durationSec, 0, 1)
    return clamp(Math.round(ratio * (count - 1)), 0, count - 1)
  }

  return 0
}

function buildTimelinePreviewTiles(
  durationSec: number,
  meta: PreviewSpriteMeta,
  visibleCount: number
): TimelinePreviewTile[] {
  const count = Math.max(1, Number(meta.count || 0))
  if (visibleCount <= 0 || count <= 0) return []

  const tiles = Array.from({ length: visibleCount }, (_, i) => {
    const ratio = visibleCount <= 1 ? 0 : i / (visibleCount - 1)
    const time = durationSec > 0 ? ratio * durationSec : 0

    return {
      index: frameIndexFromTime(time, durationSec, meta),
      time,
      label: formatTime(time),
    }
  })

  for (let i = 1; i < tiles.length; i++) {
    if (tiles[i].index < tiles[i - 1].index) {
      tiles[i].index = tiles[i - 1].index
    }
  }

  return tiles
}

function spriteSheetImageStyle(
  meta: PreviewSpriteMeta,
  frameIndex: number
): CSSProperties {
  const cols = Math.max(1, Number(meta.cols || 1))
  const rows = Math.max(1, Number(meta.rows || 1))
  const count = Math.max(1, Number(meta.count || cols * rows))

  const idx = clamp(frameIndex, 0, count - 1)
  const col = idx % cols
  const row = Math.floor(idx / cols)

  return {
    position: 'absolute',
    left: `${-col * 100}%`,
    top: `${-row * 100}%`,
    width: `${cols * 100}%`,
    height: `${rows * 100}%`,
    maxWidth: 'none',
    pointerEvents: 'none',
    userSelect: 'none',
    objectFit: 'fill',
  }
}

function spriteFrameImgSrc(meta: PreviewSpriteMeta): string {
  return String(meta.path || '').trim()
}

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

function clamp(v: number, min: number, max: number) {
  return Math.min(max, Math.max(min, v))
}

function baseName(p: string) {
  return (p || '').replaceAll('\\', '/').trim().split('/').pop() || ''
}

function stripHotPrefix(name: string) {
  return name.startsWith('HOT ') ? name.slice(4) : name
}

function formatTime(totalSeconds: number): string {
  const s = Math.max(0, Math.floor(totalSeconds))
  const h = Math.floor(s / 3600)
  const m = Math.floor((s % 3600) / 60)
  const sec = s % 60

  if (h > 0) {
    return `${h}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
  }
  return `${m}:${String(sec).padStart(2, '0')}`
}

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 pickNum(...vals: any[]): number | null {
  for (const v of vals) {
    const n = typeof v === 'string' ? Number(v) : v
    if (typeof n === 'number' && Number.isFinite(n) && n > 0) return n
  }
  return null
}

function formatFps(n?: number | null): string {
  if (!n || !Number.isFinite(n)) return '—'
  const digits = n >= 10 ? 0 : 2
  return `${n.toFixed(digits)} fps`
}

function formatResolution(h?: number | null, w?: number | null): string {
  if (w && h && Number.isFinite(w) && Number.isFinite(h)) return `${Math.round(w)}×${Math.round(h)}`
  if (h && Number.isFinite(h)) return `${Math.round(h)}p`
  return '—'
}

const reModel = /^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}/

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

  const stem = file.replace(/\.[^.]+$/, '')
  const m = stem.match(reModel)
  if (m?.[1]) return m[1]

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

function parseDateFromOutput(output?: string): Date | null {
  const fileRaw = baseName(output || '')
  const file = stripHotPrefix(fileRaw)
  if (!file) return null

  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) return null

  const mm = Number(m[1])
  const dd = Number(m[2])
  const yyyy = Number(m[3])
  const hh = Number(m[4])
  const mi = Number(m[5])
  const ss = Number(m[6])

  if (![mm, dd, yyyy, hh, mi, ss].every((n) => Number.isFinite(n))) return null
  return new Date(yyyy, mm - 1, dd, hh, mi, ss)
}

function previewIdFromJob(job: RecordJob | null): string {
  if (!job) return ''
  const file = baseName(job.output || '')
  const stem = stripHotPrefix(file.replace(/\.[^.]+$/, '')).trim()
  return String(job.status).toLowerCase() === 'running' ? String(job.id || '') : stem || String(job.id || '')
}

function previewStillSrcFromJob(job: RecordJob | null): string {
  const id = previewIdFromJob(job)
  if (!id) return ''
  return `/api/preview?id=${encodeURIComponent(id)}&file=preview.jpg`
}

function previewSpritesSrcFromJob(job: RecordJob | null): string {
  const id = previewIdFromJob(job)
  if (!id) return ''
  return `/api/preview-sprite/${encodeURIComponent(id)}`
}

function buildSegmentPreviewSrc(job: RecordJob | null, seg: Segment): string {
  const id = previewIdFromJob(job)
  if (!id) return ''

  const t = Number(seg.start || 0)
  const safeTime = Math.max(0, Number.isFinite(t) ? t : 0)

  return `/api/preview?id=${encodeURIComponent(id)}&t=${encodeURIComponent(safeTime.toFixed(3))}`
}

const AUTO_SELECTED_AI_LABELS = new Set([
  'anus_exposed',
  'female_genitalia_exposed',
  'male_genitalia_exposed',
  'female_breast_exposed',
  'buttocks_exposed',
])

const AI_SEGMENT_PADDING_SECONDS = 3

function pickFirstDefined<T = any>(...vals: T[]): T | undefined {
  for (const v of vals) {
    if (v !== undefined && v !== null) return v
  }
  return undefined
}

function extractAiPayload(data: any): any {
  if (!data || typeof data !== 'object') return null

  const candidates = [
    data?.meta?.analysis?.ai,
    data?.meta?.analysis?.AI,
    data?.analysis?.ai,
    data?.analysis?.AI,
    data?.ai,
    data?.AI,
    data?.videoAI,
    data?.videoAi,
    data?.meta?.ai,
    data?.meta?.AI,
    data?.meta?.videoAI,
    data?.meta?.videoAi,
    data?.metaJson?.analysis?.ai,
    data?.metaJson?.ai,
    data?.metadata?.analysis?.ai,
    data?.metadata?.ai,
  ]

  for (const candidate of candidates) {
    if (candidate && typeof candidate === 'object') {
      return candidate
    }
  }

  return null
}

function extractPreviewSpriteMeta(data: any): PreviewSpriteMeta | null {
  const ps =
    data?.meta?.preview?.sprite ??
    data?.preview?.sprite ??
    data?.previewSprite ??
    null

  if (!ps || typeof ps !== 'object') return null

  const path = String(ps.path || '').trim()
  const count = Number(ps.count || 0)
  const cols = Number(ps.cols || 0)
  const rows = Number(ps.rows || 0)
  const stepSeconds = Number(ps.stepSeconds || 0)

  if (!path || count <= 0 || cols <= 0 || rows <= 0) {
    return null
  }

  return {
    exists: true,
    path,
    count,
    cols,
    rows,
    stepSeconds: Number.isFinite(stepSeconds) && stepSeconds > 0 ? stepSeconds : 0,
  }
}

function extractMetaDuration(data: any): number {
  const d = Number(
    data?.meta?.media?.durationSeconds ??
    data?.media?.durationSeconds ??
    data?.durationSeconds ??
    0
  )

  return Number.isFinite(d) && d > 0 ? d : 0
}

function normalizeAiHits(
  raw: unknown,
  timeMapper?: (timeSec: number) => number
): Array<{
  time: number
  label: string
  score?: number
  start?: number
  end?: number
}> {
  if (!Array.isArray(raw)) return []

  const out: Array<{
    time: number
    label: string
    score?: number
    start?: number
    end?: number
  }> = []

  for (const item of raw) {
    if (!item || typeof item !== 'object') continue

    const x = item as any

    const time = mapOptionalTime(
      pickFirstDefined(
        x.time,
        x.Time,
        x.at,
        x.timestamp,
      ),
      timeMapper
    )

    if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) continue

    const label = String(
      pickFirstDefined(
        x.label,
        x.Label,
        x.name,
        'Treffer'
      ) || 'Treffer'
    ).trim()

    const scoreRaw = Number(
      pickFirstDefined(
        x.score,
        x.Score,
        x.confidence,
      )
    )

    const startRaw = mapOptionalTime(
      pickFirstDefined(
        x.start,
        x.Start,
        x.startSeconds,
        x.StartSeconds,
      ),
      timeMapper
    )

    const endRaw = mapOptionalTime(
      pickFirstDefined(
        x.end,
        x.End,
        x.endSeconds,
        x.EndSeconds,
      ),
      timeMapper
    )

    out.push({
      time,
      label,
      score: Number.isFinite(scoreRaw) ? scoreRaw : undefined,
      start: Number.isFinite(startRaw) ? startRaw : undefined,
      end: Number.isFinite(endRaw) ? endRaw : undefined,
    })
  }

  out.sort((a, b) => a.time - b.time)
  return out
}

function normalizeAiSegmentsFromMeta(
  raw: unknown,
  durationSec?: number,
  timeMapper?: (timeSec: number) => number
): Segment[] {
  if (!Array.isArray(raw)) return []

  const hasDuration =
    typeof durationSec === 'number' &&
    Number.isFinite(durationSec) &&
    durationSec > 0

  const out: Segment[] = []

  for (const item of raw) {
    if (!item || typeof item !== 'object') continue

    const x = item as any

    const mappedStart = mapOptionalTime(
      pickFirstDefined(
        x.startSeconds,
        x.StartSeconds,
        x.start,
        x.Start,
      ),
      timeMapper
    )

    const mappedEnd = mapOptionalTime(
      pickFirstDefined(
        x.endSeconds,
        x.EndSeconds,
        x.end,
        x.End,
      ),
      timeMapper
    )

    if (
      typeof mappedStart !== 'number' ||
      !Number.isFinite(mappedStart) ||
      typeof mappedEnd !== 'number' ||
      !Number.isFinite(mappedEnd)
    ) {
      continue
    }

    let startBase = mappedStart
    let endBase = mappedEnd

    if (endBase < startBase) {
      const tmp = startBase
      startBase = endBase
      endBase = tmp
    }

    const rawDuration = Math.max(0, endBase - startBase)
    const markerTime = startBase + rawDuration / 2

    let start = startBase - AI_SEGMENT_PADDING_SECONDS
    let end = endBase + AI_SEGMENT_PADDING_SECONDS

    if (hasDuration) {
      start = clamp(start, 0, durationSec!)
      end = clamp(end, 0, durationSec!)
    } else {
      start = Math.max(0, start)
      end = Math.max(0, end)
    }

    if (end <= start) continue

    const label = String(
      pickFirstDefined(
        x.label,
        x.Label,
        x.name,
        'Segment'
      ) || 'Segment'
    ).trim()

    out.push({
      index: out.length + 1,
      label,
      start,
      end,
      duration: Math.max(0, end - start),
      markerTime,
    })
  }

  out.sort((a, b) => {
    if ((a.markerTime ?? a.start) !== (b.markerTime ?? b.start)) {
      return (a.markerTime ?? a.start) - (b.markerTime ?? b.start)
    }
    if (a.start !== b.start) return a.start - b.start
    if (a.end !== b.end) return a.end - b.end
    return a.label.localeCompare(b.label)
  })

  return out.map((seg, idx) => ({
    ...seg,
    index: idx + 1,
  }))
}

function normalizeAiSegmentsFromHits(
  raw: unknown,
  durationSec?: number,
  timeMapper?: (timeSec: number) => number
): Segment[] {
  const hits = normalizeAiHits(raw, timeMapper)
  if (hits.length === 0) return []

  const hasDuration =
    typeof durationSec === 'number' &&
    Number.isFinite(durationSec) &&
    durationSec > 0

  const windows = hits
    .map((hit) => {
      const markerTime = Number(hit.time)

      if (!Number.isFinite(markerTime)) return null

      let start = markerTime - AI_SEGMENT_PADDING_SECONDS
      let end = markerTime + AI_SEGMENT_PADDING_SECONDS

      if (hasDuration) {
        start = clamp(start, 0, durationSec!)
        end = clamp(end, 0, durationSec!)
      } else {
        start = Math.max(0, start)
        end = Math.max(0, end)
      }

      if (end <= start) return null

      return {
        start,
        end,
        markerTime,
        labels: [String(hit.label || 'Segment').trim() || 'Segment'],
      }
    })
    .filter(Boolean) as Array<{
      start: number
      end: number
      markerTime: number
      labels: string[]
    }>

  if (windows.length === 0) return []

  windows.sort((a, b) => a.markerTime - b.markerTime)

  const merged: Array<{
    start: number
    end: number
    markerTime: number
    labels: string[]
  }> = []

  for (const win of windows) {
    const last = merged[merged.length - 1]

    if (!last || win.start > last.end) {
      merged.push({
        start: win.start,
        end: win.end,
        markerTime: win.markerTime,
        labels: [...win.labels],
      })
      continue
    }

    last.end = Math.max(last.end, win.end)
    for (const label of win.labels) {
      if (!last.labels.includes(label)) {
        last.labels.push(label)
      }
    }
  }

  return merged.map((seg, idx) => {
    const label =
      seg.labels.length <= 1
        ? seg.labels[0]
        : `${seg.labels[0]} +${seg.labels.length - 1}`

    return {
      index: idx + 1,
      label,
      start: seg.start,
      end: seg.end,
      duration: Math.max(0, seg.end - seg.start),
      markerTime: seg.markerTime,
    }
  })
}

function shouldAutoSelectAiHit(hit: {
  label: string
}): boolean {
  return AUTO_SELECTED_AI_LABELS.has(String(hit.label || '').trim().toLowerCase())
}

function segmentPairKey(aIndex: number, bIndex: number): string {
  const a = Math.min(aIndex, bIndex)
  const b = Math.max(aIndex, bIndex)
  return `${a}:${b}`
}

function buildMergedSegmentLabel(group: Segment[]): string {
  const labels = Array.from(
    new Set(
      group
        .map((seg) => getSegmentLabelText(seg.label).trim())
        .filter(Boolean)
    )
  )

  if (labels.length === 0) return `Segment ${group[0]?.index ?? 1}`
  if (labels.length === 1) return labels[0]
  return labels.join(' + ')
}

function buildSegmentGroupKey(group: Segment[]): string {
  return group.map((seg) => seg.index).join(':')
}

export default function VideoSplitModal({
  open,
  onClose,
  job,
  videoSrc,
  timelinePreviewCount = 10,
  onApply,
}: Props) {
  const videoRef = useRef<HTMLVideoElement | null>(null)
  const timelineRef = useRef<HTMLDivElement | null>(null)
  const fusionTimerRef = useRef<number | null>(null)
  const mergedPulseTimerRef = useRef<number | null>(null)
  const fusionRafRef = useRef<number | null>(null)

  const [fusionStage, setFusionStage] = useState<'idle' | 'fadeOut' | 'fadeIn'>('idle')
  const [pendingFusionPair, setPendingFusionPair] = useState<string | null>(null)
  const [enteringMergedGroupKey, setEnteringMergedGroupKey] = useState<string | null>(null)
  const [showEnteringMergedGroup, setShowEnteringMergedGroup] = useState(false)
  const [mediaDuration, setMediaDuration] = useState(0)
  const [duration, setDuration] = useState(0)
  const [currentTime, setCurrentTime] = useState(0)
  const [playing, setPlaying] = useState(false)
  const [splits, setSplits] = useState<number[]>([])
  const [busy, setBusy] = useState(false)
  const [aiError, setAiError] = useState('')
  const [applyError, setApplyError] = useState('')
  const [splitProgress, setSplitProgress] = useState<SplitApplyProgress | null>(null)
  const [deleteOriginal, setDeleteOriginal] = useState(false)
  const [connectedSegmentPairs, setConnectedSegmentPairs] = useState<string[]>([])
  const [spriteMeta, setSpriteMeta] = useState<PreviewSpriteMeta | null>(null)
  const [hoverRatio, setHoverRatio] = useState<number | null>(null)
  const [selectedSegmentIndices, setSelectedSegmentIndices] = useState<number[]>([])
  const [previewCycleTick, setPreviewCycleTick] = useState(0)
  const [aiSegments, setAiSegments] = useState<Segment[]>([])
  const [aiHits, setAiHits] = useState<Array<{
    time: number
    label: string
    score?: number
    start?: number
    end?: number
  }>>([])

  useEffect(() => {
    return () => {
      if (fusionTimerRef.current) window.clearTimeout(fusionTimerRef.current)
      if (mergedPulseTimerRef.current) window.clearTimeout(mergedPulseTimerRef.current)
      if (fusionRafRef.current) window.cancelAnimationFrame(fusionRafRef.current)
    }
  }, [])
  
  useEffect(() => {
    if (!open) return

    if (fusionTimerRef.current) window.clearTimeout(fusionTimerRef.current)
    if (mergedPulseTimerRef.current) window.clearTimeout(mergedPulseTimerRef.current)
    if (fusionRafRef.current) window.cancelAnimationFrame(fusionRafRef.current)

    setCurrentTime(0)
    setDuration(0)
    setMediaDuration(0)
    setPlaying(false)
    setSplits([])
    setSelectedSegmentIndices([])
    setAiSegments([])
    setAiHits([])
    setAiError('')
    setApplyError('')
    setSplitProgress(null)
    setDeleteOriginal(false)
    setConnectedSegmentPairs([])
    setPreviewCycleTick(0)
    setPendingFusionPair(null)
    setEnteringMergedGroupKey(null)
    setShowEnteringMergedGroup(false)
    setFusionStage('idle')
  }, [open, job?.id])

  const fusionActive =
    pendingFusionPair !== null ||
    fusionStage !== 'idle' ||
    enteringMergedGroupKey !== null

  useEffect(() => {
    if (!open || fusionActive) return

    const id = window.setInterval(() => {
      setPreviewCycleTick((prev) => prev + 1)
    }, 3000)

    return () => {
      window.clearInterval(id)
    }
  }, [open, fusionActive])

  useEffect(() => {
    const video = videoRef.current
    if (!video || !open) return

    const onLoaded = () => {
      const d = Number(video.duration || 0)
      setDuration(Number.isFinite(d) ? d : 0)
    }

    const onTime = () => {
      setCurrentTime(video.currentTime || 0)
    }

    const onPlay = () => setPlaying(true)
    const onPause = () => setPlaying(false)

    video.addEventListener('loadedmetadata', onLoaded)
    video.addEventListener('durationchange', onLoaded)
    video.addEventListener('timeupdate', onTime)
    video.addEventListener('play', onPlay)
    video.addEventListener('pause', onPause)

    return () => {
      video.removeEventListener('loadedmetadata', onLoaded)
      video.removeEventListener('durationchange', onLoaded)
      video.removeEventListener('timeupdate', onTime)
      video.removeEventListener('play', onPlay)
      video.removeEventListener('pause', onPause)
    }
  }, [open, videoSrc])

  useEffect(() => {
    if (!open || !job) {
      setSpriteMeta(null)
      setAiHits([])
      setAiSegments([])
      setMediaDuration(0)
      return
    }

    const file = baseName(job.output || '')
    if (!file) {
      setSpriteMeta(null)
      setAiHits([])
      setAiSegments([])
      setMediaDuration(0)
      setSplits([])
      return
    }

    let cancelled = false

    ;(async () => {
      try {
        const res = await fetch(`/api/record/done/meta?file=${encodeURIComponent(file)}`, {
          cache: 'no-store',
        })
        if (!res.ok) {
          if (!cancelled) {
            setSpriteMeta(null)
            setMediaDuration(0)
          }
          return
        }

        const data = await res.json().catch(() => null)

        const ps = extractPreviewSpriteMeta(data)
        const ai = extractAiPayload(data)
        const clipMap = extractPreviewClipMap(data)

        const metaDuration = extractMetaDuration(data)
        const resolvedDuration = metaDuration > 0 ? metaDuration : 0

        const shouldMapPreviewAi =
          Boolean(shouldMapAiPreviewTimes(ai, clipMap, resolvedDuration) && clipMap)

        const mapAiTimes =
          shouldMapPreviewAi && clipMap
            ? (timeSec: number) => mapPreviewTimeToVideoTime(timeSec, clipMap)
            : undefined

        const metaAiHits = normalizeAiHits(
          ai?.hits ?? ai?.Hits ?? [],
          mapAiTimes
        )

        const metaAiSegmentsFromMeta = normalizeAiSegmentsFromMeta(
          ai?.segments ?? ai?.Segments ?? [],
          resolvedDuration,
          mapAiTimes
        )

        const metaAiSegments =
          metaAiSegmentsFromMeta.length > 0
            ? metaAiSegmentsFromMeta
            : normalizeAiSegmentsFromHits(
                ai?.hits ?? ai?.Hits ?? [],
                resolvedDuration,
                mapAiTimes
              )

        if (!cancelled) {
          setMediaDuration(metaDuration > 0 ? metaDuration : 0)
          setAiHits(metaAiHits)
          setAiSegments(metaAiSegments)

          if (metaAiSegments.length > 0) {
            setSplits([])
          }

          setSpriteMeta(ps)
        }
      } catch {
        if (!cancelled) {
          setSpriteMeta(null)
          setMediaDuration(0)
        }
      }
    })()

    return () => {
      cancelled = true
    }
  }, [open, job?.id, job?.output])

  const effectiveDuration = mediaDuration > 0 ? mediaDuration : duration

  const updateHoverFromClientX = useCallback((clientX: number) => {
    const el = timelineRef.current
    if (!el) return

    const rect = el.getBoundingClientRect()
    if (rect.width <= 0) return

    const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
    setHoverRatio(ratio)
  }, [])

  const onTimelinePointerMove = useCallback(
    (e: PointerEvent<HTMLDivElement>) => {
      updateHoverFromClientX(e.clientX)
    },
    [updateHoverFromClientX]
  )

  const onTimelinePointerEnter = useCallback(
    (e: PointerEvent<HTMLDivElement>) => {
      updateHoverFromClientX(e.clientX)
    },
    [updateHoverFromClientX]
  )

  const onTimelinePointerLeave = useCallback(() => {
    setHoverRatio(null)
  }, [])

  const seekTo = useCallback(
    (next: number) => {
      const video = videoRef.current
      if (!video) return
      const clamped = clamp(next, 0, Math.max(0, effectiveDuration || 0))
      video.currentTime = clamped
      setCurrentTime(clamped)
    },
    [effectiveDuration]
  )

  const togglePlayback = useCallback(async () => {
    const video = videoRef.current
    if (!video) return

    if (video.paused) {
      try {
        await video.play()
      } catch {
        // ignore
      }
    } else {
      video.pause()
    }
  }, [])

  const addSplitAt = useCallback(
    (rawTime: number) => {
      if (busy) return

      const d = effectiveDuration || 0
      if (!Number.isFinite(d) || d <= 0) return

      const t = clamp(rawTime, 0, d)

      if (t <= 0.2 || t >= d - 0.2) return

      setAiSegments([])
      setSplits((prev) => {
        const exists = prev.some((x) => Math.abs(x - t) < 0.2)
        if (exists) return prev
        return [...prev, t].sort((a, b) => a - b)
      })
    },
    [effectiveDuration, busy]
  )

  const removeSplit = useCallback((time: number) => {
    if (busy) return
    setAiSegments([])
    setSplits((prev) => prev.filter((x) => x !== time))
  }, [busy])

  const clearSplits = useCallback(() => {
    if (busy) return
    setAiSegments([])
    setSplits([])
  }, [busy])

  const onTimelineClick = useCallback(
    (e: MouseEvent<HTMLDivElement>) => {
      if (busy) return
      if (!timelineRef.current || effectiveDuration <= 0) return

      const rect = timelineRef.current.getBoundingClientRect()
      const ratio = clamp((e.clientX - rect.left) / rect.width, 0, 1)

      setHoverRatio(ratio)

      const t = ratio * effectiveDuration
      seekTo(t)
    },
    [effectiveDuration, seekTo, busy]
  )

  const manualSegments = useMemo<Segment[]>(() => {
    const points = [0, ...splits, effectiveDuration].filter((v) => Number.isFinite(v))
    const out: Segment[] = []

    for (let i = 0; i < points.length - 1; i++) {
      const start = points[i]
      const end = points[i + 1]
      const segDuration = Math.max(0, end - start)

      out.push({
        index: i + 1,
        label: `Segment ${i + 1}`,
        start,
        end,
        duration: segDuration,
      })
    }

    return out
  }, [splits, effectiveDuration])

  const segments = useMemo<Segment[]>(() => {
    if (aiSegments.length > 0) {
      return aiSegments.map((seg, idx) => ({
        ...seg,
        index: idx + 1,
      }))
    }

    return manualSegments
  }, [aiSegments, manualSegments])

  const displaySegmentGroups = useMemo(() => {
    if (segments.length === 0) return [] as Array<{
      key: string
      segments: Segment[]
    }>

    const ordered = [...segments].sort((a, b) => a.index - b.index)
    const groups: Array<{
      key: string
      segments: Segment[]
    }> = []

    let currentGroup: Segment[] = [ordered[0]]

    for (let i = 1; i < ordered.length; i++) {
      const prev = ordered[i - 1]
      const next = ordered[i]

      const connected =
        next.index === prev.index + 1 &&
        connectedSegmentPairs.includes(segmentPairKey(prev.index, next.index))

      if (connected) {
        currentGroup.push(next)
      } else {
        groups.push({
          key: currentGroup.map((seg) => seg.index).join(':'),
          segments: currentGroup,
        })
        currentGroup = [next]
      }
    }

    groups.push({
      key: currentGroup.map((seg) => seg.index).join(':'),
      segments: currentGroup,
    })

    return groups
  }, [segments, connectedSegmentPairs])

  const isGroupFullySelected = useCallback(
    (group: Segment[]) => group.every((seg) => selectedSegmentIndices.includes(seg.index)),
    [selectedSegmentIndices]
  )

  const isGroupPartiallySelected = useCallback(
    (group: Segment[]) =>
      group.some((seg) => selectedSegmentIndices.includes(seg.index)) &&
      !group.every((seg) => selectedSegmentIndices.includes(seg.index)),
    [selectedSegmentIndices]
  )

  const toggleSegmentGroupSelection = useCallback((group: Segment[]) => {
    if (busy) return

    const groupIndices = group.map((seg) => seg.index)

    setSelectedSegmentIndices((prev) => {
      const allSelected = groupIndices.every((idx) => prev.includes(idx))

      if (allSelected) {
        return prev.filter((idx) => !groupIndices.includes(idx))
      }

      return Array.from(new Set([...prev, ...groupIndices])).sort((a, b) => a - b)
    })
  }, [busy])

  const adjacentSegmentPairs = useMemo(() => {
    return segments.slice(0, -1).map((seg, idx) => {
      const next = segments[idx + 1]
      return {
        left: seg,
        right: next,
        key: segmentPairKey(seg.index, next.index),
      }
    })
  }, [segments])

  useEffect(() => {
    const validKeys = new Set(adjacentSegmentPairs.map((pair) => pair.key))
    setConnectedSegmentPairs((prev) => prev.filter((key) => validKeys.has(key)))
  }, [adjacentSegmentPairs])

  const connectSegmentGroups = useCallback(
    (leftGroup: Segment[], rightGroup: Segment[]) => {
      if (busy || leftGroup.length === 0 || rightGroup.length === 0) return

      const leftLast = leftGroup[leftGroup.length - 1]
      const rightFirst = rightGroup[0]
      const pairKey = segmentPairKey(leftLast.index, rightFirst.index)
      const mergedKey = buildSegmentGroupKey([...leftGroup, ...rightGroup])

      setSelectedSegmentIndices((prev) =>
        Array.from(
          new Set([
            ...prev,
            ...leftGroup.map((seg) => seg.index),
            ...rightGroup.map((seg) => seg.index),
          ])
        ).sort((a, b) => a - b)
      )

      if (fusionTimerRef.current) window.clearTimeout(fusionTimerRef.current)
      if (mergedPulseTimerRef.current) window.clearTimeout(mergedPulseTimerRef.current)
      if (fusionRafRef.current) window.cancelAnimationFrame(fusionRafRef.current)

      setPendingFusionPair(pairKey)
      setEnteringMergedGroupKey(null)
      setShowEnteringMergedGroup(false)
      setFusionStage('fadeOut')

      fusionTimerRef.current = window.setTimeout(() => {
        setConnectedSegmentPairs((prev) =>
          prev.includes(pairKey) ? prev : [...prev, pairKey]
        )

        setPendingFusionPair(null)
        setEnteringMergedGroupKey(mergedKey)
        setShowEnteringMergedGroup(false)
        setFusionStage('fadeIn')

        fusionRafRef.current = window.requestAnimationFrame(() => {
          fusionRafRef.current = window.requestAnimationFrame(() => {
            setShowEnteringMergedGroup(true)
          })
        })

        mergedPulseTimerRef.current = window.setTimeout(() => {
          setFusionStage('idle')
          setEnteringMergedGroupKey(null)
          setShowEnteringMergedGroup(false)
        }, 220)
      }, 180)
    },
    [busy]
  )

  const disconnectSegmentGroup = useCallback((group: Segment[]) => {
    if (busy || group.length < 2) return

    const keysToRemove = new Set<string>()

    for (let i = 0; i < group.length - 1; i++) {
      keysToRemove.add(segmentPairKey(group[i].index, group[i + 1].index))
    }

    setConnectedSegmentPairs((prev) => prev.filter((key) => !keysToRemove.has(key)))
  }, [busy])

  const timelinePreviewTiles = useMemo(() => {
    if (!spriteMeta?.exists || !spriteMeta.path) return []

    const count = Math.max(1, Number(spriteMeta.count || 0))
    const visibleCount = Math.min(Math.max(1, timelinePreviewCount), count)

    return buildTimelinePreviewTiles(effectiveDuration, spriteMeta, visibleCount)
  }, [spriteMeta, effectiveDuration, timelinePreviewCount])

  const selectedSegments = useMemo(() => {
    return segments.filter((seg) => selectedSegmentIndices.includes(seg.index))
  }, [segments, selectedSegmentIndices])

  const segmentGroupsToApply = useMemo<Segment[][]>(() => {
    if (selectedSegments.length === 0) return []

    const ordered = [...selectedSegments].sort((a, b) => a.index - b.index)
    const groups: Segment[][] = []

    let currentGroup: Segment[] = [ordered[0]]

    for (let i = 1; i < ordered.length; i++) {
      const prev = ordered[i - 1]
      const next = ordered[i]

      const directlyAdjacent = next.index === prev.index + 1
      const connected =
        directlyAdjacent &&
        connectedSegmentPairs.includes(segmentPairKey(prev.index, next.index))

      if (connected) {
        currentGroup.push(next)
      } else {
        groups.push(currentGroup)
        currentGroup = [next]
      }
    }

    groups.push(currentGroup)
    return groups
  }, [selectedSegments, connectedSegmentPairs])

  const segmentsToApply = useMemo<Segment[]>(() => {
    return segmentGroupsToApply.map((group, idx) => {
      const start = group[0].start
      const end = group[group.length - 1].end

      return {
        index: idx + 1,
        label: buildMergedSegmentLabel(group),
        start,
        end,
        duration: Math.max(0, end - start),
        markerTime: start + (end - start) / 2,
      }
    })
  }, [segmentGroupsToApply])

  const selectedSegmentApplyOrderMap = useMemo(() => {
    const map = new Map<number, number>()

    segmentGroupsToApply.forEach((group, groupIdx) => {
      group.forEach((seg) => {
        map.set(seg.index, groupIdx + 1)
      })
    })

    return map
  }, [segmentGroupsToApply])

  const splitStateBySegmentIndex = useMemo(() => {
    const out = new Map<number, { state: 'queued' | 'running' | 'done'; progress: number }>()

    if (!busy || !splitProgress) return out

    const currentOrder = Number(splitProgress.current || 0)
    const completed = Number(splitProgress.completed || 0)
    const runningOrder =
      splitProgress.segment?.index != null
        ? Number(splitProgress.segment.index)
        : splitProgress.phase === 'splitting'
          ? currentOrder
          : null

    const runningProgress = clamp(splitProgress.segmentProgress ?? 0, 0, 1)

    for (const seg of selectedSegments) {
      const order = selectedSegmentApplyOrderMap.get(seg.index) ?? 0

      if (splitProgress.phase === 'done' || (order > 0 && order <= completed)) {
        out.set(seg.index, { state: 'done', progress: 1 })
        continue
      }

      if (runningOrder != null && order === runningOrder) {
        out.set(seg.index, { state: 'running', progress: runningProgress })
        continue
      }

      out.set(seg.index, { state: 'queued', progress: 0 })
    }

    return out
  }, [busy, splitProgress, selectedSegments, selectedSegmentApplyOrderMap])

  useEffect(() => {
    if (segments.length === 0) {
      setSelectedSegmentIndices([])
      return
    }

    if (aiSegments.length === 0) {
      setSelectedSegmentIndices((prev) => {
        const valid = prev.filter((idx) => segments.some((seg) => seg.index === idx))
        if (valid.length > 0) return valid
        return segments.map((seg) => seg.index)
      })
      return
    }

    const autoMatchedSegmentIndices = segments
      .filter((seg) =>
        aiHits.some((hit) => {
          if (!shouldAutoSelectAiHit(hit)) return false

          const hitStart = hit.start ?? hit.time
          const hitEnd = hit.end ?? hit.time

          return hitEnd >= seg.start && hitStart <= seg.end
        })
      )
      .map((seg) => seg.index)

    if (autoMatchedSegmentIndices.length > 0) {
      setSelectedSegmentIndices(autoMatchedSegmentIndices)
      return
    }

    setSelectedSegmentIndices((prev) => {
      const valid = prev.filter((idx) => segments.some((seg) => seg.index === idx))
      if (valid.length > 0) return valid
      return segments.map((seg) => seg.index)
    })
  }, [segments, aiHits, aiSegments.length])

  const hoverTime = useMemo(() => {
    if (hoverRatio == null || effectiveDuration <= 0) return null
    return clamp(hoverRatio, 0, 1) * effectiveDuration
  }, [hoverRatio, effectiveDuration])

  const hoverFrameIndex = useMemo(() => {
    if (!spriteMeta?.exists || !spriteMeta.path || hoverTime == null) return null
    return frameIndexFromTime(hoverTime, effectiveDuration, spriteMeta)
  }, [spriteMeta, hoverTime, effectiveDuration])

  const selectAllSegments = useCallback(() => {
    if (busy) return
    setSelectedSegmentIndices(segments.map((seg) => seg.index))
  }, [segments, busy])

  const clearSegmentSelection = useCallback(() => {
    if (busy) return
    setSelectedSegmentIndices([])
  }, [busy])

  const currentRatio =
    effectiveDuration > 0
      ? clamp(currentTime / effectiveDuration, 0, 1)
      : 0

  const title = job ? `Video schneiden – ${baseName(job.output || '')}` : 'Video schneiden'

  const previewStillSrc = useMemo(() => previewStillSrcFromJob(job), [job])
  const previewSpritesSrc = useMemo(() => previewSpritesSrcFromJob(job), [job])

  const anyJob = job as any
  const model = useMemo(() => modelNameFromOutput(job?.output), [job?.output])
  const file = useMemo(() => stripHotPrefix(baseName(job?.output || '')), [job?.output])

  const videoWidth = pickNum(anyJob?.videoWidth, anyJob?.width, anyJob?.meta?.width)
  const videoHeight = pickNum(anyJob?.videoHeight, anyJob?.height, anyJob?.meta?.height)
  const fps = pickNum(anyJob?.fps, anyJob?.frameRate, anyJob?.meta?.fps, anyJob?.meta?.frameRate)

  const resolutionLabel = formatResolution(videoHeight, videoWidth)
  const fpsLabel = formatFps(fps)
  const sizeLabel = formatBytes(
    pickNum(anyJob?.sizeBytes, anyJob?.fileSizeBytes, anyJob?.bytes, anyJob?.size)
  )

  const dateLabel = useMemo(() => {
    const fromName = parseDateFromOutput(job?.output)
    if (fromName) return formatDateTime(fromName)

    return formatDateTime(
      anyJob?.startedAt ??
        anyJob?.endedAt ??
        anyJob?.createdAt ??
        anyJob?.fileCreatedAt ??
        anyJob?.ctime ??
        null
    )
  }, [job?.output, anyJob?.startedAt, anyJob?.endedAt, anyJob?.createdAt, anyJob?.fileCreatedAt, anyJob?.ctime])

  return (
    <Modal
      open={open}
      onClose={onClose}
      title={title}
      width="max-w-[96vw]"
      footer={
        <>
          <Button variant="secondary" onClick={onClose}>
            Schließen
          </Button>
          
          <Button
            variant="primary"
            disabled={!job || !videoSrc || busy || segmentsToApply.length === 0}
            onClick={async () => {
              if (!job) return

              setBusy(true)
              setApplyError('')
              setSplitProgress({
                phase: 'preparing',
                total: segmentsToApply.length,
                current: 0,
                completed: 0,
                segmentProgress: 0,
                overallProgress: 0,
                message: 'Split wird vorbereitet…',
              })

              try {
                await onApply?.({
                  job,
                  splits: [],
                  segments: segmentsToApply,
                  deleteOriginal,
                  onProgress: (progress) => {
                    setSplitProgress(progress)
                  },
                })

                // absichtlich NICHT automatisch schließen
                // onClose()
              } catch (err) {
                const message =
                  err instanceof Error ? err.message : 'Split fehlgeschlagen.'

                setApplyError(message)
                setSplitProgress((prev) => ({
                  phase: 'error',
                  total: prev?.total ?? segmentsToApply.length,
                  current: prev?.current ?? 0,
                  completed: prev?.completed ?? 0,
                  segmentProgress: prev?.segmentProgress ?? 0,
                  overallProgress: prev?.overallProgress ?? 0,
                  message,
                  segment: prev?.segment,
                }))
              } finally {
                setBusy(false)
              }
            }}
          >
            {segmentsToApply.length === 1
              ? '1 Datei erzeugen'
              : `${segmentsToApply.length} Dateien erzeugen`}
          </Button>
        </>
      }
    >
      <div className="p-4 sm:p-6 xl:h-[75vh] xl:max-h-[75vh] xl:overflow-hidden">
        <div className="grid gap-4 xl:grid-cols-[300px_minmax(0,1fr)_320px] xl:h-full xl:min-h-0">
          <aside className="min-h-0 xl:h-full rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5 xl:flex xl:flex-col xl:overflow-hidden">
            <div className="space-y-4">
              <div className="overflow-hidden rounded-xl bg-black ring-1 ring-black/10 dark:ring-white/10">
                {previewStillSrc ? (
                  <img
                    src={previewStillSrc}
                    alt=""
                    className="block aspect-video w-full object-contain bg-black"
                    draggable={false}
                  />
                ) : (
                  <div className="grid aspect-video place-items-center bg-black text-sm text-white/70">
                    Kein Preview verfügbar
                  </div>
                )}
              </div>

              <div className="space-y-1">
                <div className="truncate text-lg font-semibold text-gray-900 dark:text-white">{model}</div>
                <div className="break-all text-xs text-gray-500 dark:text-gray-400">{file || '—'}</div>
              </div>

              <div className="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
                <div className="text-gray-500 dark:text-gray-400">Status</div>
                <div className="font-medium text-gray-900 dark:text-white">{job?.status || '—'}</div>

                <div className="text-gray-500 dark:text-gray-400">Auflösung</div>
                <div className="font-medium text-gray-900 dark:text-white">{resolutionLabel}</div>

                <div className="text-gray-500 dark:text-gray-400">FPS</div>
                <div className="font-medium text-gray-900 dark:text-white">{fpsLabel}</div>

                <div className="text-gray-500 dark:text-gray-400">Laufzeit</div>
                <div className="font-medium text-gray-900 dark:text-white">
                  {effectiveDuration > 0 ? formatTime(effectiveDuration) : '—'}
                </div>

                <div className="text-gray-500 dark:text-gray-400">Größe</div>
                <div className="font-medium text-gray-900 dark:text-white">{sizeLabel}</div>

                <div className="text-gray-500 dark:text-gray-400">Datum</div>
                <div className="font-medium text-gray-900 dark:text-white">{dateLabel}</div>
              </div>
            </div>
          </aside>

          <section className="min-w-0 xl:h-full xl:min-h-0 xl:flex xl:flex-col xl:space-y-4">
            <div className="overflow-hidden rounded-xl bg-black ring-1 ring-black/10 dark:ring-white/10">
              {videoSrc ? (
                <video
                  ref={videoRef}
                  src={videoSrc}
                  controls
                  preload="metadata"
                  className="mx-auto block h-auto max-h-[46vh] w-full rounded-xl bg-black object-contain"
                />
              ) : (
                <div className="grid h-[320px] place-items-center text-sm text-white/70">
                  Kein Video verfügbar
                </div>
              )}
            </div>

            <div className="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-white/5 xl:flex-1 xl:min-h-0">
              <div className="mb-3 flex flex-wrap items-center gap-2">
                <Button variant="secondary" onClick={() => seekTo(currentTime - 5)} disabled={!videoSrc || busy}>
                  -5s
                </Button>
                <Button variant="primary" onClick={togglePlayback} disabled={!videoSrc || busy}>
                  {playing ? 'Pause' : 'Play'}
                </Button>
                <Button variant="secondary" onClick={() => seekTo(currentTime + 5)} disabled={!videoSrc || busy}>
                  +5s
                </Button>
                <Button variant="secondary" onClick={() => addSplitAt(currentTime)} disabled={!videoSrc || effectiveDuration <= 0 || busy}>
                  Split bei aktueller Zeit
                </Button>
                <Button variant="secondary" onClick={clearSplits} disabled={splits.length === 0 || busy}>
                  Alle Splits löschen
                </Button>

                <div className="ml-auto text-sm text-gray-600 dark:text-gray-300">
                  {formatTime(currentTime)} / {formatTime(effectiveDuration)}
                </div>
              </div>

              {aiError ? (
                <div className="mb-3 rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700 ring-1 ring-red-200 dark:bg-red-500/10 dark:text-red-200 dark:ring-red-400/20">
                  {aiError}
                </div>
              ) : null}

              {applyError ? (
                <div className="mb-3 rounded-xl bg-red-50 px-3 py-2 text-sm text-red-700 ring-1 ring-red-200 dark:bg-red-500/10 dark:text-red-200 dark:ring-red-400/20">
                  {applyError}
                </div>
              ) : null}

              <div className="space-y-3">
                <div className="relative">
                <div
                  ref={timelineRef}
                  className="relative h-24 cursor-pointer select-none overflow-hidden rounded-xl ring-1 ring-gray-200 dark:ring-white/10"
                  onClick={onTimelineClick}
                  onPointerMove={onTimelinePointerMove}
                  onPointerEnter={onTimelinePointerEnter}
                  onPointerLeave={onTimelinePointerLeave}
                  onPointerDown={(e) => {
                    e.stopPropagation()
                  }}
                  title="Klicken zum Springen"
                  >
                    {spriteMeta?.exists && spriteMeta.path && timelinePreviewTiles.length > 0 ? (
                      <div
                        className="absolute inset-0 grid gap-0 bg-black/20"
                        style={{
                          gridTemplateColumns: `repeat(${timelinePreviewTiles.length}, minmax(0, 1fr))`,
                        }}
                        aria-hidden="true"
                      >
                        {timelinePreviewTiles.map((tile, i) => (
                          <div
                            key={`${tile.index}-${i}-${tile.time}`}
                            className="relative h-full overflow-hidden"
                            title={tile.label}
                          >
                            <div className="absolute inset-0 overflow-hidden bg-black">
                              <img
                                src={spriteFrameImgSrc(spriteMeta)}
                                alt=""
                                aria-hidden="true"
                                draggable={false}
                                style={spriteSheetImageStyle(spriteMeta, tile.index)}
                              />
                            </div>
                            <div className="absolute inset-0 bg-black/10 dark:bg-black/20" />
                          </div>
                        ))}
                      </div>
                    ) : previewSpritesSrc ? (
                      <div
                        className="absolute inset-0 bg-left bg-no-repeat opacity-90"
                        style={{
                          backgroundImage: `url("${previewSpritesSrc}")`,
                          backgroundSize: 'auto 100%',
                        }}
                        aria-hidden="true"
                      />
                    ) : (
                      <div className="absolute inset-0 bg-gray-100 dark:bg-white/10" />
                    )}

                    {spriteMeta?.exists && spriteMeta.path && hoverFrameIndex != null && hoverTime != null ? (
                      <div
                        className="pointer-events-none absolute bottom-full z-40 mb-2"
                        style={{
                          left: `clamp(56px, ${(hoverRatio ?? 0) * 100}%, calc(100% - 56px))`,
                          transform: 'translateX(-50%)',
                        }}
                      >
                        <div className="overflow-hidden rounded-lg border border-white/15 bg-black shadow-2xl">
                          <div className="relative h-20 w-36 overflow-hidden bg-black">
                            <div className="absolute inset-0 overflow-hidden bg-black">
                              <img
                                src={spriteFrameImgSrc(spriteMeta)}
                                alt=""
                                aria-hidden="true"
                                draggable={false}
                                style={spriteSheetImageStyle(spriteMeta, hoverFrameIndex)}
                              />
                            </div>
                          </div>

                          <div className="bg-black/85 px-2 py-1 text-center text-[11px] font-medium text-white">
                            {formatTime(hoverTime)}
                          </div>
                        </div>

                        <div className="mx-auto -mt-1 h-2 w-2 rotate-45 border-b border-r border-white/15 bg-black" />
                      </div>
                    ) : null}

                    <div className="pointer-events-none absolute inset-0 bg-white/30 dark:bg-black/30" />

                    <div
                      className="pointer-events-none absolute inset-y-0 left-0 bg-indigo-500/20 dark:bg-indigo-400/20"
                      style={{ width: `${currentRatio * 100}%` }}
                    />

                    {displaySegmentGroups.map((group) => {
                      if (effectiveDuration <= 0) return null

                      const firstSeg = group.segments[0]
                      const lastSeg = group.segments[group.segments.length - 1]

                      const left = (firstSeg.start / effectiveDuration) * 100
                      const width = ((lastSeg.end - firstSeg.start) / effectiveDuration) * 100
                      const safeWidth = Math.max(width, 0.35)

                      const selected = isGroupFullySelected(group.segments)
                      const partiallySelected = isGroupPartiallySelected(group.segments)

                      const mergedLabel = buildMergedSegmentLabel(group.segments)
                      const cleanLabel = mergedLabel
                      const topGroupLabelItem = getHighestPrioritySegmentLabelItem(
                        group.segments.map((seg) => seg.label)
                      )

                      const showTextLabel = width >= 8
                      const isTinySegment = width < 8

                      return (
                        <div
                          key={`segment-overlay-group-${group.key}`}
                          className="pointer-events-none absolute inset-y-0"
                          style={{
                            left: `${left}%`,
                            width: `${safeWidth}%`,
                          }}
                          title={`${cleanLabel} · ${formatTime(firstSeg.start)}–${formatTime(lastSeg.end)}`}
                        >
                          <span
                            className={cn(
                              'absolute inset-y-0 inset-x-0 rounded-sm ring-1',
                              selected
                                ? 'bg-amber-400/20 ring-amber-300/55'
                                : partiallySelected
                                  ? 'bg-amber-400/15 ring-amber-300/40'
                                  : 'bg-amber-400/10 ring-amber-300/25'
                            )}
                          />

                          <span
                            className={cn(
                              'absolute inset-y-0 left-0 w-[2px]',
                              selected ? 'bg-amber-300/80' : 'bg-amber-300/35'
                            )}
                          />

                          <span
                            className={cn(
                              'absolute inset-y-0 right-0 w-[2px]',
                              selected ? 'bg-amber-300/80' : 'bg-amber-300/35'
                            )}
                          />

                          <div
                            className={cn(
                              'absolute top-2 z-10 flex',
                              isTinySegment ? 'left-1/2 -translate-x-1/2' : 'inset-x-1 justify-center'
                            )}
                          >
                            <span
                              className={cn(
                                'inline-flex items-center rounded-full font-semibold backdrop-blur-sm',
                                selected
                                  ? 'bg-amber-400/90 text-black shadow'
                                  : 'bg-black/45 text-white/90',
                                showTextLabel
                                  ? 'max-w-full gap-1 truncate px-2 py-1 text-[10px]'
                                  : 'h-8 w-8 justify-center p-0'
                              )}
                            >
                              <span className={cn('inline-flex items-center', showTextLabel ? 'gap-1' : '')}>
                                {(() => {
                                  const Icon = topGroupLabelItem.icon

                                  return (
                                    <span
                                      className={cn(
                                        'inline-flex items-center justify-center rounded-full',
                                        showTextLabel ? 'size-4' : 'size-5'
                                      )}
                                      title={topGroupLabelItem.text}
                                    >
                                      <Icon
                                        className={cn(
                                          showTextLabel ? 'size-5 shrink-0' : 'size-5'
                                        )}
                                        aria-hidden="true"
                                      />
                                    </span>
                                  )
                                })()}

                                {showTextLabel ? <span className="truncate">{cleanLabel}</span> : null}
                              </span>
                            </span>
                          </div>
                        </div>
                      )
                    })}

                    {aiSegments.length === 0 && splits.map((time) => {
                      const left = effectiveDuration > 0 ? (time / effectiveDuration) * 100 : 0
                      return (
                        <button
                          key={time}
                          type="button"
                          className="absolute top-0 h-full -translate-x-1/2"
                          style={{ left: `${left}%` }}
                          onClick={(e) => {
                            if (busy) return
                            e.preventDefault()
                            e.stopPropagation()
                            seekTo(time)
                          }}
                          title={`Split ${formatTime(time)}`}
                        >
                          <span className="absolute inset-y-0 left-1/2 w-[2px] -translate-x-1/2 bg-rose-500" />
                          <span className="absolute left-1/2 top-1 -translate-x-1/2 rounded-full bg-rose-500 px-1.5 py-0.5 text-[10px] font-semibold text-white shadow">
                            |
                          </span>
                        </button>
                      )
                    })}

                    <div
                      className="pointer-events-none absolute top-0 h-full w-[2px] bg-indigo-600 dark:bg-indigo-300"
                      style={{ left: `${currentRatio * 100}%` }}
                    />

                    <div className="absolute inset-x-0 bottom-0 flex items-center justify-between bg-black/45 px-2 py-1 text-[11px] font-medium text-white">
                      <span>0:00</span>
                      <span>{formatTime(effectiveDuration / 2)}</span>
                      <span>{formatTime(effectiveDuration)}</span>
                    </div>
                  </div>
                </div>

                {aiSegments.length === 0 && (
                  <div className="flex flex-wrap gap-2">
                    {splits.length === 0 ? (
                      <div className="text-sm text-gray-500 dark:text-gray-400">
                        Noch keine Splits gesetzt.
                      </div>
                    ) : (
                      splits.map((time, idx) => (
                        <div
                          key={time}
                          className="inline-flex items-center gap-2 rounded-full bg-gray-100 px-3 py-1.5 text-sm ring-1 ring-gray-200 dark:bg-white/10 dark:ring-white/10"
                        >
                          <button
                            type="button"
                            className="font-medium text-gray-900 hover:underline disabled:cursor-not-allowed disabled:opacity-50 dark:text-white"
                            onClick={() => {
                              if (busy) return
                              seekTo(time)
                            }}
                            disabled={busy}
                          >
                            Split {idx + 1}: {formatTime(time)}
                          </button>
                          <button
                            type="button"
                            className="text-red-600 hover:underline disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-300"
                            onClick={() => removeSplit(time)}
                            disabled={busy}
                          >
                            entfernen
                          </button>
                        </div>
                      ))
                    )}
                  </div>
                )}
              </div>
            </div>
          </section>

          <aside className="min-h-0 xl:h-full xl:overflow-y-auto xl:overflow-x-hidden xl:overscroll-contain rounded-2xl border border-gray-200 bg-white p-4 pr-3 shadow-sm dark:border-white/10 dark:bg-white/5">
            <div className="space-y-3 overflow-x-hidden">
              <div className="flex items-center justify-between gap-2">
                <div className="text-sm font-semibold text-gray-900 dark:text-white">
                  Segmente ({selectedSegments.length}/{segments.length}) • Ausgabe: {segmentsToApply.length}
                </div>

                {segments.length > 0 ? (
                  <div className="text-xs text-gray-500 dark:text-gray-400">
                    Nur ausgewählte Segmente werden gesplittet
                  </div>
                ) : null}
              </div>

              {segments.length > 0 ? (
                <div className="flex flex-wrap gap-2">
                  <Button variant="secondary" onClick={selectAllSegments} disabled={busy}>
                    Alle wählen
                  </Button>
                  <Button variant="secondary" onClick={clearSegmentSelection} disabled={busy}>
                    Auswahl aufheben
                  </Button>
                </div>
              ) : null}
            </div>
            
            <div className="mt-3 mb-3 shrink-0 space-y-3">
              <div className="rounded-xl bg-gray-50 px-3 py-3 ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">
                <label
                  className={cn(
                    'flex items-start gap-3',
                    busy ? 'cursor-not-allowed opacity-60' : 'cursor-pointer'
                  )}
                >
                  <Checkbox
                    checked={deleteOriginal}
                    disabled={busy}
                    onChange={setDeleteOriginal}
                    className="mt-0.5 shrink-0"
                  />

                  <span className="min-w-0">
                    <span className="block text-sm font-medium text-gray-900 dark:text-white">
                      Originaldatei löschen
                    </span>
                    <span className="mt-1 block text-xs text-gray-500 dark:text-gray-400">
                      Nach erfolgreichem Split wird die Originaldatei entfernt.
                    </span>
                  </span>
                </label>
              </div>
            </div>

            <div className="space-y-3">
              {displaySegmentGroups.map((group, groupIdx) => {
                const firstSeg = group.segments[0]
                const lastSeg = group.segments[group.segments.length - 1]
                const mergedLabel = buildMergedSegmentLabel(group.segments)

                const previewSources = Array.from(
                  new Set(
                    group.segments
                      .map((seg) => buildSegmentPreviewSrc(job, seg))
                      .filter(Boolean)
                  )
                )

                const isConnectedGroup = group.segments.length > 1
                const activePreviewIndex =
                  isConnectedGroup && previewSources.length > 1
                    ? (previewCycleTick + groupIdx) % previewSources.length
                    : 0

                const selected = isGroupFullySelected(group.segments)
                const partiallySelected = isGroupPartiallySelected(group.segments)

                const splitUi = splitStateBySegmentIndex.get(firstSeg.index)
                const splitState = splitUi?.state
                const splitPercent = Math.round((splitUi?.progress ?? 0) * 100)

                const nextGroup = displaySegmentGroups[groupIdx + 1] ?? null
                const prevGroup = displaySegmentGroups[groupIdx - 1] ?? null
                
                const canConnectToNext =
                  nextGroup &&
                  lastSeg.index + 1 === nextGroup.segments[0].index

                const fusingIntoNext =
                  nextGroup &&
                  pendingFusionPair === segmentPairKey(lastSeg.index, nextGroup.segments[0].index)

                const fusingFromPrev =
                  prevGroup &&
                  pendingFusionPair ===
                    segmentPairKey(
                      prevGroup.segments[prevGroup.segments.length - 1].index,
                      firstSeg.index
                    )

                const isFusingConnector = Boolean(
                  nextGroup &&
                    pendingFusionPair ===
                      segmentPairKey(lastSeg.index, nextGroup.segments[0].index)
                )

                const isFadingOut = (fusingIntoNext || fusingFromPrev) && fusionStage === 'fadeOut'
                const isEnteringMerged = enteringMergedGroupKey === group.key

                const fusionCardClass = cn(
                  isFadingOut && 'opacity-0 scale-[0.985]',
                  isEnteringMerged && !showEnteringMergedGroup && 'opacity-0 scale-[0.985]',
                  isEnteringMerged && showEnteringMergedGroup && 'opacity-100 scale-100'
                )

                const topGroupLabelItem = getHighestPrioritySegmentLabelItem(
                  group.segments.map((seg) => seg.label)
                )

                return (
                  <div key={group.key} className="space-y-2">
                    <div
                      role="button"
                      aria-disabled={busy}
                      tabIndex={busy ? -1 : 0}
                      className={cn(
                        'group relative block w-full overflow-hidden rounded-2xl border text-left transform-gpu transition-[transform,opacity] duration-200 ease-out',
                        busy && 'cursor-not-allowed',
                        selected
                          ? 'border-indigo-400/60 bg-indigo-500/10 shadow-[0_0_0_1px_rgba(99,102,241,0.08)] dark:border-indigo-400/45'
                          : partiallySelected
                            ? 'border-indigo-300/40 bg-indigo-500/5 dark:border-indigo-400/25'
                            : 'border-gray-200 bg-white hover:border-gray-300 dark:border-white/10 dark:bg-white/5 dark:hover:border-white/15',
                        splitState === 'running' && 'ring-2 ring-indigo-400/60',
                        splitState === 'done' && 'border-emerald-400/60 bg-emerald-500/10 dark:border-emerald-400/45',
                        fusionCardClass
                      )}
                      onClick={() => {
                        if (busy) return
                        toggleSegmentGroupSelection(group.segments)
                      }}
                      onKeyDown={(e) => {
                        if (busy) return
                        if (e.key === 'Enter' || e.key === ' ') {
                          e.preventDefault()
                          toggleSegmentGroupSelection(group.segments)
                        }
                      }}
                    >
                      <div className="relative aspect-video overflow-hidden">
                        {previewSources.length > 0 ? (
                          <>
                            {previewSources.map((src, previewIdx) => (
                              <img
                                key={`${group.key}-${previewIdx}-${src}`}
                                src={src}
                                alt={mergedLabel}
                                draggable={false}
                                className={cn(
                                  'absolute inset-0 h-full w-full object-cover transition-opacity duration-200 ease-linear',
                                  previewIdx === activePreviewIndex ? 'opacity-100' : 'opacity-0',
                                  !selected ? 'grayscale-60' : ''
                                )}
                              />
                            ))}
                          </>
                        ) : (
                          <div className="absolute inset-0 grid place-items-center bg-black text-xs text-white/70">
                            Kein Preview
                          </div>
                        )}

                        <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/35 to-black/20" />

                        <div className="absolute inset-0 flex flex-col justify-between p-3">
                          <div className="flex items-start gap-2">
                            <div
                              className="shrink-0"
                              onClick={(e) => e.stopPropagation()}
                              onPointerDown={(e) => e.stopPropagation()}
                            >
                              <Checkbox
                                checked={selected}
                                disabled={busy}
                                onChange={() => {
                                  if (busy) return
                                  toggleSegmentGroupSelection(group.segments)
                                }}
                                ariaLabel={`${mergedLabel} auswählen`}
                                className="size-5"
                              />
                            </div>

                            <div className="min-w-0 flex-1">
                              <div className="flex items-start gap-2">
                                <div className="min-w-0 flex-1">
                                  <div
                                    className="flex items-center gap-2 text-sm font-semibold text-white"
                                    style={{
                                      textShadow:
                                        '0 1px 2px rgba(0,0,0,0.95), 0 0 6px rgba(0,0,0,0.85), 0 0 12px rgba(0,0,0,0.55)',
                                    }}
                                  >
                                    {(() => {
                                      const Icon = topGroupLabelItem.icon

                                      return (
                                        <span
                                          className="inline-flex size-5 items-center justify-center rounded-full"
                                          title={topGroupLabelItem.text}
                                        >
                                          <Icon className="size-4 shrink-0 text-white" aria-hidden="true" />
                                        </span>
                                      )
                                    })()}

                                    <span className="truncate">{mergedLabel}</span>
                                  </div>
                                </div>

                                <div className="flex shrink-0 items-center gap-2">
                                  {busy && selected && splitState ? (
                                    <span
                                      className={cn(
                                        'rounded-full px-2.5 py-1 text-[11px] font-semibold backdrop-blur-sm',
                                        splitState === 'queued' && 'bg-white/90 text-gray-900 ring-1 ring-gray-200',
                                        splitState === 'running' && 'bg-indigo-600 text-white',
                                        splitState === 'done' && 'bg-emerald-600 text-white'
                                      )}
                                    >
                                      {splitState === 'queued' && 'Wartet'}
                                      {splitState === 'running' && `${splitPercent}%`}
                                      {splitState === 'done' && 'Fertig'}
                                    </span>
                                  ) : null}

                                  {isConnectedGroup ? (
                                    <button
                                      type="button"
                                      disabled={busy}
                                      title="Verbindung lösen"
                                      onClick={(e) => {
                                        e.preventDefault()
                                        e.stopPropagation()
                                        disconnectSegmentGroup(group.segments)
                                      }}
                                      className={cn(
                                        'inline-flex size-8 shrink-0 items-center justify-center rounded-full ring-1 backdrop-blur-sm transition-all duration-200',
                                        busy
                                          ? 'cursor-not-allowed opacity-50'
                                          : 'cursor-pointer bg-indigo-600/90 text-white ring-indigo-400/60 hover:scale-105 hover:bg-indigo-500'
                                      )}
                                    >
                                      <LinkIcon className="size-4" />
                                    </button>
                                  ) : null}
                                </div>
                              </div>
                            </div>
                          </div>

                          <div className="flex items-center justify-between gap-2">
                            <div className="rounded-full bg-black/45 px-2.5 py-1 text-[11px] font-medium text-white/90 backdrop-blur-sm">
                              {group.segments.length > 1 ? (
                                `${group.segments.length} verbundene Segmente`
                              ) : null}
                            </div>

                            <button
                              type="button"
                              className="
                                rounded-full cursor-pointer select-none
                                px-2.5 py-1 text-[11px] font-medium backdrop-blur-sm transition
                                bg-white/95 text-gray-900 ring-1 ring-gray-200 shadow-sm hover:bg-white
                                disabled:cursor-not-allowed disabled:opacity-50
                                dark:bg-black/70 dark:text-white dark:ring-white/10 dark:hover:bg-black/80
                              "
                              disabled={busy}
                              onClick={(e) => {
                                if (busy) return
                                e.preventDefault()
                                e.stopPropagation()
                                seekTo(firstSeg.start)
                              }}
                              title={`Zur Position ${formatTime(firstSeg.start)} springen`}
                            >
                              {formatTime(firstSeg.start)} → {formatTime(lastSeg.end)}
                            </button>
                          </div>

                          {busy && selected && splitState ? (
                            <div className="absolute inset-x-0 bottom-0 h-1 bg-black/25">
                              <div
                                className={cn(
                                  'h-full transition-[width] duration-150',
                                  splitState === 'queued' && 'bg-white/40',
                                  splitState === 'running' && 'bg-indigo-400',
                                  splitState === 'done' && 'bg-emerald-400'
                                )}
                                style={{
                                  width: `${splitState === 'done' ? 100 : splitState === 'running' ? splitPercent : 0}%`,
                                }}
                              />
                            </div>
                          ) : null}
                        </div>
                      </div>
                    </div>

                    {canConnectToNext ? (
                      <div className="relative flex h-9 items-center gap-2 px-1 overflow-hidden">
                        <div
                          className={cn(
                            'relative z-10 h-px flex-1 bg-gray-200 transition-opacity duration-200 ease-out dark:bg-white/10',
                            isFusingConnector && 'opacity-0'
                          )}
                        />

                        <button
                          type="button"
                          disabled={busy || Boolean(isFusingConnector)}
                          title="Segmente verbinden"
                          onClick={(e) => {
                            e.preventDefault()
                            e.stopPropagation()
                            connectSegmentGroups(group.segments, nextGroup.segments)
                          }}
                          className={cn(
                            'relative z-10 inline-flex size-9 items-center justify-center rounded-full ring-1 transition-opacity duration-200 ease-out',
                            busy || isFusingConnector
                              ? 'cursor-not-allowed'
                              : 'cursor-pointer bg-gray-50 text-gray-600 ring-gray-200 hover:bg-indigo-50 hover:text-indigo-700 hover:ring-indigo-200 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-indigo-500/10 dark:hover:text-indigo-200 dark:hover:ring-indigo-400/20',
                            isFusingConnector && 'opacity-0'
                          )}
                        >
                          <LinkIcon className="size-4" />
                        </button>

                        <div
                          className={cn(
                            'relative z-10 h-px flex-1 bg-gray-200 transition-opacity duration-200 ease-out dark:bg-white/10',
                            isFusingConnector && 'opacity-0'
                          )}
                        />
                      </div>
                    ) : null}
                  </div>
                )
              })}

              {displaySegmentGroups.length === 0 ? (
                <div className="rounded-xl bg-gray-50 px-3 py-3 text-sm text-gray-500 ring-1 ring-gray-200 dark:bg-white/5 dark:text-gray-400 dark:ring-white/10">
                  Noch keine Segmente vorhanden.
                </div>
              ) : null}
            </div>
          </aside>
        </div>
      </div>
    </Modal>
  )
}