// frontend/src/components/ui/TrainingTab.tsx
'use client'

import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType, type CSSProperties } from 'react'
import Button from './Button'
import LoadingSpinner from './LoadingSpinner'
import { formatDuration } from './formatters'
import {
  ArrowPathIcon,
  BoltIcon,
  ClockIcon,
  ForwardIcon,
  TrashIcon,
  XCircleIcon,
} from '@heroicons/react/20/solid'
import { getSegmentLabelItem } from './Icons'
import Modal from './Modal'
import { createPortal } from 'react-dom'

type DrawingTrainingBox = TrainingBox & {
  startX: number
  startY: number
}

type ScoredLabel = {
  label: string
  score: number
}

type TrainingDetectorStatus = {
  trainCount: number
  valCount: number
  requiredTrain: number
  requiredVal: number
  datasetReady: boolean
  dataReady: boolean
  modelExists: boolean
  modelPath?: string
  source?: string
}

type TrainingStatus = {
  feedbackCount: number
  requiredCount: number
  canTrain: boolean
  training?: TrainingJobStatus
  detector?: TrainingDetectorStatus
}

type TrainingJobStatus = {
  running: boolean
  progress: number
  step: string
  message?: string
  error?: string
  startedAt?: string
  finishedAt?: string
  durationMs?: number
  stage?: string
  epoch?: number
  epochs?: number
}

type TrainingPrediction = {
  modelAvailable: boolean
  source?: string

  sexPosition: string
  sexPositionScore: number
  peoplePresent: ScoredLabel[]
  bodyPartsPresent: ScoredLabel[]
  objectsPresent: ScoredLabel[]
  clothingPresent: ScoredLabel[]
  boxes?: TrainingBox[]
}

type TrainingSample = {
  sampleId: string
  frameUrl: string
  sourceFile: string
  sourcePath?: string
  sourceSizeBytes?: number
  second: number
  createdAt: string
  prediction: TrainingPrediction
}

type TrainingLabels = {
  people: string[]
  sexPositions: string[]
  bodyParts: string[]
  objects: string[]
  clothing: string[]
}

type CorrectionState = {
  sexPosition: string
  peoplePresent: string[]
  bodyPartsPresent: string[]
  objectsPresent: string[]
  clothingPresent: string[]
  boxes: TrainingBox[]
}

type TrainingBox = {
  label: string
  score?: number
  x: number
  y: number
  w: number
  h: number
}

type BoxInteraction =
  | {
      type: 'move'
      index: number
      startX: number
      startY: number
      original: TrainingBox
    }
  | {
      type: 'resize'
      index: number
      handle: 'nw' | 'ne' | 'sw' | 'se'
      startX: number
      startY: number
      original: TrainingBox
    }

type MagnifierState = {
  visible: boolean
  clientX: number
  clientY: number
  imageX: number
  imageY: number
}

type TrainingConfidence = {
  score: number
  level: 'none' | 'low' | 'mid' | 'high'
  label: string
}

type TrainingLabelStat = {
  label: string
  count: number
  confidence?: TrainingConfidence
}

type TrainingStats = {
  feedbackCount: number
  acceptedCount: number
  correctedCount: number
  sampleCount: number
  boxCount: number
  modelAvailable: boolean
  confidence?: TrainingConfidence
  labels: {
    people: TrainingLabelStat[]
    sexPositions: TrainingLabelStat[]
    bodyParts: TrainingLabelStat[]
    objects: TrainingLabelStat[]
    clothing: TrainingLabelStat[]
  }
}

type TrainingNoticeKind = 'success' | 'error' | 'info' | 'warning'

type TrainingNoticeItem = {
  icon: ComponentType<{
    className?: string
    'aria-hidden'?: boolean | 'true' | 'false'
  }>
  label: string
  value: string
}

type TrainingNotice = {
  kind: TrainingNoticeKind
  title: string
  message: string
  items?: TrainingNoticeItem[]
  progress?: number
}

function trainingNoticeClass(kind: TrainingNoticeKind) {
  switch (kind) {
    case 'success':
      return {
        wrap: 'border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-400/30 dark:bg-emerald-500/10 dark:text-emerald-100',
        icon: 'bg-emerald-500 text-white',
        detail: 'text-emerald-800/80 dark:text-emerald-100/70',
      }
    case 'error':
      return {
        wrap: 'border-red-200 bg-red-50 text-red-900 dark:border-red-400/30 dark:bg-red-500/10 dark:text-red-100',
        icon: 'bg-red-500 text-white',
        detail: 'text-red-800/80 dark:text-red-100/70',
      }
    case 'warning':
      return {
        wrap: 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-400/30 dark:bg-amber-500/10 dark:text-amber-100',
        icon: 'bg-amber-500 text-white',
        detail: 'text-amber-800/80 dark:text-amber-100/70',
      }
    default:
      return {
        wrap: 'border-indigo-200 bg-indigo-50 text-indigo-900 dark:border-indigo-400/30 dark:bg-indigo-500/10 dark:text-indigo-100',
        icon: 'bg-indigo-500 text-white',
        detail: 'text-indigo-800/80 dark:text-indigo-100/70',
      }
  }
}

function TrainingNoticeCard(props: {
  notice: TrainingNotice
  onClose?: () => void
}) {
  const cls = trainingNoticeClass(props.notice.kind)

  const icon =
    props.notice.kind === 'success'
      ? '✓'
      : props.notice.kind === 'error'
        ? '!'
        : props.notice.kind === 'warning'
          ? '⚠'
          : 'i'

  return (
    <div
      className={[
        'rounded-xl border px-3 py-2 shadow-sm',
        cls.wrap,
      ].join(' ')}
      role={props.notice.kind === 'error' ? 'alert' : 'status'}
      aria-live={props.notice.kind === 'error' ? 'assertive' : 'polite'}
    >
      <div className="flex items-start gap-3">
        <div
          className={[
            'mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-black',
            cls.icon,
          ].join(' ')}
          aria-hidden="true"
        >
          {icon}
        </div>

        <div className="min-w-0 flex-1">
          <div className="flex flex-wrap items-center gap-2 text-sm font-semibold">
            <span>{props.notice.title}</span>

            {typeof props.notice.progress === 'number' ? (
              <span className="rounded-full bg-black/5 px-2 py-0.5 text-[11px] font-bold ring-1 ring-black/10 dark:bg-white/10 dark:ring-white/10">
                {Math.round(clampPercent(props.notice.progress))}%
              </span>
            ) : null}
          </div>

          {props.notice.items?.length ? (
            <div className="mt-2 grid grid-cols-1 gap-1.5 text-xs sm:grid-cols-2">
              {props.notice.items.map((item) => {
                const Icon = item.icon

                return (
                  <div
                    key={`${item.label}-${item.value}`}
                    className="flex min-w-0 items-center gap-2 rounded-lg bg-black/5 px-2 py-1.5 ring-1 ring-black/5 dark:bg-white/10 dark:ring-white/10"
                  >
                    <Icon
                      className="h-4 w-4 shrink-0 opacity-80"
                      aria-hidden="true"
                    />

                    <span className="min-w-0 flex-1 truncate opacity-75">
                      {item.label}
                    </span>

                    <span className="shrink-0 font-bold">
                      {item.value}
                    </span>
                  </div>
                )
              })}
            </div>
          ) : (
            <div className="mt-0.5 break-words text-xs leading-snug">
              {props.notice.message}
            </div>
          )}
        </div>

        {props.onClose ? (
          <button
            type="button"
            onClick={props.onClose}
            className="shrink-0 rounded-lg px-2 py-1 text-xs font-semibold opacity-70 transition hover:bg-black/5 hover:opacity-100 dark:hover:bg-white/10"
            aria-label="Meldung schließen"
            title="Meldung schließen"
          >
            ✕
          </button>
        ) : null}
      </div>
    </div>
  )
}

function backendText(data: any, fallback: string) {
  return String(
    data?.message ||
    data?.error ||
    data?.detail ||
    fallback
  ).trim()
}

function trainingDurationMs(job?: TrainingJobStatus | null) {
  const direct = Number(job?.durationMs)

  if (Number.isFinite(direct) && direct > 0) {
    return direct
  }

  const started = job?.startedAt ? Date.parse(job.startedAt) : NaN
  const finished = job?.finishedAt ? Date.parse(job.finishedAt) : NaN

  if (!Number.isFinite(started) || !Number.isFinite(finished)) {
    return 0
  }

  return Math.max(0, finished - started)
}

function countPercent(count: number, total: number) {
  if (!Number.isFinite(count) || !Number.isFinite(total) || total <= 0) return '0%'
  return `${Math.round((count / total) * 100)}%`
}

function confidencePercent(confidence?: TrainingConfidence | null) {
  const score = Number(confidence?.score)
  if (!Number.isFinite(score)) return '0%'
  return `${Math.round(clamp01(score) * 100)}%`
}

function confidenceLabel(confidence?: TrainingConfidence | null) {
  return confidence?.label || 'Keine'
}

function confidencePillClass(confidence?: TrainingConfidence | null) {
  switch (confidence?.level) {
    case 'high':
      return 'bg-emerald-50 text-emerald-800 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-100 dark:ring-emerald-400/30'
    case 'mid':
      return 'bg-yellow-50 text-yellow-900 ring-yellow-200 dark:bg-yellow-500/15 dark:text-yellow-100 dark:ring-yellow-400/30'
    case 'low':
      return 'bg-red-50 text-red-800 ring-red-200 dark:bg-red-500/15 dark:text-red-100 dark:ring-red-400/30'
    default:
      return 'bg-gray-50 text-gray-700 ring-gray-200 dark:bg-white/5 dark:text-gray-200 dark:ring-white/10'
  }
}

function averageCategoryConfidence(values: TrainingLabelStat[]) {
  const scores = values
    .map((item) => Number(item.confidence?.score))
    .filter((score) => Number.isFinite(score))

  if (scores.length === 0) {
    return undefined
  }

  const avg = scores.reduce((sum, score) => sum + clamp01(score), 0) / scores.length

  return confidenceFromScore(avg)
}

function confidenceFromScore(score: number): TrainingConfidence {
  const safeScore = clamp01(Number(score))

  if (safeScore >= 0.75) {
    return {
      score: safeScore,
      level: 'high',
      label: 'Hoch',
    }
  }

  if (safeScore >= 0.45) {
    return {
      score: safeScore,
      level: 'mid',
      label: 'Mittel',
    }
  }

  if (safeScore > 0) {
    return {
      score: safeScore,
      level: 'low',
      label: 'Niedrig',
    }
  }

  return {
    score: 0,
    level: 'none',
    label: 'Keine',
  }
}

function currentAnalysisConfidence(prediction?: TrainingPrediction | null): TrainingConfidence {
  if (!prediction?.modelAvailable) {
    return confidenceFromScore(0)
  }

  const scores: number[] = []

  const addScore = (value: unknown) => {
    const n = Number(value)
    if (!Number.isFinite(n)) return
    if (n <= 0) return

    scores.push(clamp01(n))
  }

  // YOLO-Detector: Sexposition als Full-Frame-/Positions-Label
  if (
    prediction.sexPosition &&
    prediction.sexPosition !== 'unknown'
  ) {
    addScore(prediction.sexPositionScore)
  }

  // Detector: sichtbare Boxen sind der wichtigste Signalträger.
  for (const box of prediction.boxes ?? []) {
    addScore(box.score)
  }

  // Fallback, falls Labels Scores haben, aber keine Boxen vorhanden sind.
  if ((prediction.boxes ?? []).length === 0) {
    for (const item of prediction.bodyPartsPresent ?? []) {
      addScore(item.score)
    }

    for (const item of prediction.objectsPresent ?? []) {
      addScore(item.score)
    }

    for (const item of prediction.clothingPresent ?? []) {
      addScore(item.score)
    }
  }

  if (scores.length === 0) {
    return confidenceFromScore(0)
  }

  const avg = scores.reduce((sum, score) => sum + score, 0) / scores.length

  return confidenceFromScore(avg)
}

const emptyLabels: TrainingLabels = {
  people: [],
  sexPositions: ['unknown'],
  bodyParts: [],
  objects: [],
  clothing: [],
}

function percent(v: number) {
  if (!Number.isFinite(v)) return '—'
  return `${Math.round(v * 100)}%`
}

function scoreLevel(score?: number | null): 'none' | 'low' | 'mid' | 'high' {
  const n = Number(score)

  if (!Number.isFinite(n)) return 'none'
  if (n < 0.5) return 'low'
  if (n < 0.75) return 'mid'
  return 'high'
}

function scoreBorderClass(score?: number | null, opts?: { draft?: boolean }) {
  if (opts?.draft) return 'border-amber-400'

  switch (scoreLevel(score)) {
    case 'low':
      return 'border-red-500'
    case 'mid':
      return 'border-yellow-400'
    case 'high':
      return 'border-emerald-400'
    default:
      return 'border-gray-300'
  }
}

function scoreRingClass(score?: number | null, opts?: { draft?: boolean }) {
  if (opts?.draft) return 'ring-amber-500'

  switch (scoreLevel(score)) {
    case 'low':
      return 'ring-red-500'
    case 'mid':
      return 'ring-yellow-400'
    case 'high':
      return 'ring-emerald-500'
    default:
      return 'ring-gray-400'
  }
}

function scoreDetectionPillClass(score?: number | null) {
  switch (scoreLevel(score)) {
    case 'low':
      return 'bg-red-50 text-red-800 ring-red-200 dark:bg-red-500/15 dark:text-red-100 dark:ring-red-400/30'
    case 'mid':
      return 'bg-yellow-50 text-yellow-900 ring-yellow-200 dark:bg-yellow-500/15 dark:text-yellow-100 dark:ring-yellow-400/30'
    case 'high':
      return 'bg-emerald-50 text-emerald-800 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-100 dark:ring-emerald-400/30'
    default:
      return 'bg-gray-50 text-gray-700 ring-gray-200 dark:bg-white/5 dark:text-gray-200 dark:ring-white/10'
  }
}

function detectorBoxAppearance(label: string) {
  const clean = String(label || '').trim()

  if (clean === 'person_female' || clean === 'female_person') {
    return {
      activeSurface:
        'dark:bg-pink-500/10 dark:shadow-[0_0_0_1px_rgba(244,114,182,0.20),0_10px_24px_rgba(2,6,23,0.38)]',
      idleHover: 'dark:hover:bg-pink-500/[0.05]',
      line: 'bg-pink-400',
      lineHover: 'dark:group-hover:bg-pink-400/70',
      iconActive:
        'dark:bg-pink-500/15 dark:text-pink-100 dark:ring-pink-400/25',
      iconIdle:
        'dark:text-pink-200/80 dark:group-hover:bg-pink-500/10 dark:group-hover:text-pink-100 dark:group-hover:ring-pink-400/20',
      selectedText: 'dark:text-pink-300',
    }
  }

  if (clean === 'person_male' || clean === 'male_person') {
    return {
      activeSurface:
        'dark:bg-sky-500/10 dark:shadow-[0_0_0_1px_rgba(56,189,248,0.20),0_10px_24px_rgba(2,6,23,0.38)]',
      idleHover: 'dark:hover:bg-sky-500/[0.05]',
      line: 'bg-sky-400',
      lineHover: 'dark:group-hover:bg-sky-400/70',
      iconActive:
        'dark:bg-sky-500/15 dark:text-sky-100 dark:ring-sky-400/25',
      iconIdle:
        'dark:text-sky-200/80 dark:group-hover:bg-sky-500/10 dark:group-hover:text-sky-100 dark:group-hover:ring-sky-400/20',
      selectedText: 'dark:text-sky-300',
    }
  }

  return {
    activeSurface:
      'dark:bg-indigo-500/10 dark:shadow-[0_0_0_1px_rgba(129,140,248,0.20),0_10px_24px_rgba(2,6,23,0.38)]',
    idleHover: 'dark:hover:bg-indigo-500/[0.05]',
    line: 'bg-indigo-400',
    lineHover: 'dark:group-hover:bg-indigo-400/70',
    iconActive:
      'dark:bg-indigo-500/15 dark:text-indigo-100 dark:ring-indigo-400/25',
    iconIdle:
      'dark:text-indigo-200/80 dark:group-hover:bg-indigo-500/10 dark:group-hover:text-indigo-100 dark:group-hover:ring-indigo-400/20',
    selectedText: 'dark:text-indigo-300',
  }
}

function normalizeMovedBox(box: TrainingBox): TrainingBox {
  const w = clamp01(box.w)
  const h = clamp01(box.h)

  return {
    ...box,
    x: Math.max(0, Math.min(1 - w, Number(box.x) || 0)),
    y: Math.max(0, Math.min(1 - h, Number(box.y) || 0)),
    w,
    h,
  }
}

function clampPercent(v: number) {
  if (!Number.isFinite(v)) return 0
  return Math.max(0, Math.min(100, v))
}

function toggleArrayValue(arr: string[], value: string) {
  return arr.includes(value)
    ? arr.filter((x) => x !== value)
    : [...arr, value]
}

function clamp01(v: number) {
  if (!Number.isFinite(v)) return 0
  return Math.max(0, Math.min(1, v))
}

function snap01(v: number, epsilon = 0.006) {
  const n = clamp01(v)

  if (n <= epsilon) return 0
  if (n >= 1 - epsilon) return 1

  return n
}

function normalizeBox(box: TrainingBox): TrainingBox {
  const x = clamp01(box.x)
  const y = clamp01(box.y)
  const w = clamp01(Math.min(box.w, 1 - x))
  const h = clamp01(Math.min(box.h, 1 - y))

  return {
    ...box,
    x,
    y,
    w,
    h,
  }
}

function boxGeometryChanged(a: TrainingBox, b: TrainingBox) {
  const epsilon = 0.0005

  return (
    Math.abs(a.x - b.x) > epsilon ||
    Math.abs(a.y - b.y) > epsilon ||
    Math.abs(a.w - b.w) > epsilon ||
    Math.abs(a.h - b.h) > epsilon
  )
}

function markBoxCorrected(box: TrainingBox): TrainingBox {
  const { score: _oldScore, ...boxWithoutScore } = box
  return boxWithoutScore
}

function uniqStrings(values: string[]) {
  const seen = new Set<string>()
  const out: string[] = []

  for (const value of values) {
    const clean = String(value || '').trim()
    if (!clean || seen.has(clean)) continue
    seen.add(clean)
    out.push(clean)
  }

  return out
}

function peopleLabelsFromBoxes(boxes: TrainingBox[], labels: TrainingLabels) {
  return uniqStrings(
    boxes
      .map((box) => String(box.label || '').trim())
      .filter((label) => labels.people.includes(label))
  )
}

function predictionToCorrection(sample: TrainingSample | null): CorrectionState {
  const p = sample?.prediction

  const boxes = (p?.boxes ?? [])
    .map((box) => ({
      label: String(box.label || '').trim(),
      score: box.score,
      x: clamp01(Number(box.x)),
      y: clamp01(Number(box.y)),
      w: clamp01(Number(box.w)),
      h: clamp01(Number(box.h)),
    }))
    .filter((box) => box.label && box.w > 0 && box.h > 0)

  return {
    sexPosition: p?.sexPosition || 'unknown',
    peoplePresent: (p?.peoplePresent ?? []).map((x) => x.label),
    bodyPartsPresent: (p?.bodyPartsPresent ?? []).map((x) => x.label),
    objectsPresent: (p?.objectsPresent ?? []).map((x) => x.label),
    clothingPresent: (p?.clothingPresent ?? []).map((x) => x.label),
    boxes,
  }
}

function applyBoxLabelToCorrection(
  state: CorrectionState,
  label: string,
  labels: TrainingLabels
): CorrectionState {
  const clean = String(label || '').trim()
  if (!clean) return state

  if (labels.people.includes(clean)) {
    return {
      ...state,
      peoplePresent: state.peoplePresent.includes(clean)
        ? state.peoplePresent
        : [...state.peoplePresent, clean],
    }
  }

  if (labels.bodyParts.includes(clean)) {
    return {
      ...state,
      bodyPartsPresent: state.bodyPartsPresent.includes(clean)
        ? state.bodyPartsPresent
        : [...state.bodyPartsPresent, clean],
    }
  }

  if (labels.objects.includes(clean)) {
    return {
      ...state,
      objectsPresent: state.objectsPresent.includes(clean)
        ? state.objectsPresent
        : [...state.objectsPresent, clean],
    }
  }

  if (labels.clothing.includes(clean)) {
    return {
      ...state,
      clothingPresent: state.clothingPresent.includes(clean)
        ? state.clothingPresent
        : [...state.clothingPresent, clean],
    }
  }

  return state
}

function removeBoxLabelFromCorrection(
  state: CorrectionState,
  label: string,
  labels: TrainingLabels
): CorrectionState {
  const clean = String(label || '').trim()
  if (!clean) return state

  const remainingBoxesWithSameLabel = (state.boxes ?? []).some(
    (box) => String(box.label || '').trim() === clean
  )

  // Badge nur abwählen, wenn keine weitere Box mit diesem Label existiert.
  if (remainingBoxesWithSameLabel) return state

  if (labels.people.includes(clean)) {
    return {
      ...state,
      peoplePresent: state.peoplePresent.filter((x) => x !== clean),
    }
  }

  if (labels.bodyParts.includes(clean)) {
    return {
      ...state,
      bodyPartsPresent: state.bodyPartsPresent.filter((x) => x !== clean),
    }
  }

  if (labels.objects.includes(clean)) {
    return {
      ...state,
      objectsPresent: state.objectsPresent.filter((x) => x !== clean),
    }
  }

  if (labels.clothing.includes(clean)) {
    return {
      ...state,
      clothingPresent: state.clothingPresent.filter((x) => x !== clean),
    }
  }

  return state
}

function removeBoxFromCorrection(
  state: CorrectionState,
  index: number,
  labels: TrainingLabels
): CorrectionState {
  const boxes = state.boxes ?? []
  const removed = boxes[index]

  if (!removed) return state

  const removedLabel = String(removed.label || '').trim()

  const next: CorrectionState = {
    ...state,
    boxes: boxes.filter((_, i) => i !== index),
  }

  return removeBoxLabelFromCorrection(next, removedLabel, labels)
}

function changeBoxLabelInCorrection(
  state: CorrectionState,
  index: number,
  nextLabel: string,
  labels: TrainingLabels
): CorrectionState {
  const boxes = state.boxes ?? []
  const currentBox = boxes[index]
  const cleanNextLabel = String(nextLabel || '').trim()

  if (!currentBox || !cleanNextLabel) return state

  const oldLabel = String(currentBox.label || '').trim()
  if (oldLabel === cleanNextLabel) return state

  let next: CorrectionState = {
    ...state,
    boxes: boxes.map((box, i) => {
      if (i !== index) return box

      const { score: _oldScore, ...boxWithoutScore } = box

      return {
        ...boxWithoutScore,
        label: cleanNextLabel,
      }
    }),
  }

  next = removeBoxLabelFromCorrection(next, oldLabel, labels)
  next = applyBoxLabelToCorrection(next, cleanNextLabel, labels)

  return next
}

function sortLabelList(values?: string[], opts?: { keepUnknownFirst?: boolean }) {
  const list = [...(values ?? [])].sort((a, b) =>
    a.localeCompare(b, undefined, { sensitivity: 'base' })
  )

  if (!opts?.keepUnknownFirst) return list

  return [
    ...list.filter((x) => x === 'unknown'),
    ...list.filter((x) => x !== 'unknown'),
  ]
}

function sortTrainingLabels(input: Partial<TrainingLabels> | null | undefined): TrainingLabels {
  return {
    people: sortLabelList(input?.people),
    sexPositions: sortLabelList(input?.sexPositions, { keepUnknownFirst: true }),
    bodyParts: sortLabelList(input?.bodyParts),
    objects: sortLabelList(input?.objects),
    clothing: sortLabelList(input?.clothing),
  }
}

function TrainingOverlay(props: { step: string; progress: number }) {
  return (
    <div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
      <div className="absolute inset-0 rounded-lg bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />

      <div className="relative z-10 flex flex-col items-center justify-center">
        <LoadingSpinner
          size="lg"
          className="text-white"
          srLabel="Training läuft…"
        />

        <div className="mt-3 text-sm font-semibold">
          Training läuft…
        </div>

        <div className="mt-1 max-w-[260px] px-4 text-xs text-white/80">
          {props.step || 'Bitte warten. Die Oberfläche ist währenddessen gesperrt.'}
        </div>

        <div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-white/20">
          <div
            className="h-full rounded-full bg-emerald-400 transition-all duration-500"
            style={{ width: `${clampPercent(props.progress)}%` }}
          />
        </div>

        <div className="mt-1 text-[11px] text-white/70">
          {Math.round(props.progress)}%
        </div>
      </div>
    </div>
  )
}

function LoadingImageOverlay(props: {
  text?: string
  progress?: number
}) {
  const progress = clampPercent(props.progress ?? 0)

  return (
    <div className="absolute inset-0 z-20 flex items-center justify-center text-center text-white">
      <div className="absolute inset-0 rounded-lg bg-black/45 backdrop-blur-[8px] shadow-[inset_0_0_72px_30px_rgba(0,0,0,0.75)]" />

      <div className="relative z-10 flex flex-col items-center justify-center">
        <LoadingSpinner
          size="lg"
          className="text-white"
          srLabel="Analyse läuft…"
        />

        <div className="mt-3 text-sm font-semibold">
          Analyse läuft…
        </div>

        <div className="mt-1 max-w-[260px] px-4 text-xs text-white/80">
          {props.text || 'Bild wird erstellt und analysiert. Bitte warten.'}
        </div>

        <div className="mt-3 h-2 w-48 overflow-hidden rounded-full bg-white/20">
          <div
            className="h-full rounded-full bg-indigo-400 transition-all duration-500"
            style={{ width: `${progress}%` }}
          />
        </div>

        <div className="mt-1 text-[11px] text-white/70">
          {Math.round(progress)}%
        </div>
      </div>
    </div>
  )
}

function labelTileClass(active: boolean) {
  return [
    'group flex min-h-[58px] w-full flex-col items-center justify-center gap-1 rounded-xl px-2 py-1.5 text-center text-[10px] font-semibold leading-tight ring-1 transition sm:min-h-[74px] sm:py-2',
    'focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 dark:focus:ring-offset-gray-900',
    active
      ? [
          'bg-indigo-100 text-indigo-900 ring-2 ring-indigo-500 shadow-sm',
          'hover:bg-indigo-200',
          'dark:bg-indigo-500/30 dark:text-indigo-50 dark:ring-indigo-300/70',
          'dark:hover:bg-indigo-500/40',
        ].join(' ')
      : [
          'bg-white text-gray-700 ring-gray-200 hover:bg-gray-50 hover:text-gray-900',
          'dark:bg-white/5 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10 dark:hover:text-white',
        ].join(' '),
  ].join(' ')
}

function LabelToggleGrid(props: {
  values: string[]
  selected: string[]
  scores?: Record<string, number>
  onToggle: (value: string) => void
  drawLabel?: string
  onDrawLabelChange?: (value: string) => void
  disabled?: boolean
  gridClassName?: string
}) {
  if (props.values.length === 0) {
    return (
      <div className="rounded-lg bg-gray-50 px-3 py-2 text-[11px] text-gray-500 ring-1 ring-black/5 dark:bg-white/5 dark:text-gray-400 dark:ring-white/10">
        Keine Einträge verfügbar.
      </div>
    )
  }

  return (
    <div className={props.gridClassName || 'grid grid-cols-2 gap-2 sm:grid-cols-3'}>
      {props.values.map((value) => {
        const active = props.selected.includes(value)
        const item = getSegmentLabelItem(value)
        const Icon = item.icon
        const score = props.scores?.[value]
        const hasScore = typeof score === 'number' && Number.isFinite(score)
        const isDrawLabel = props.drawLabel === value

        return (
          <button
            key={value}
            type="button"
            aria-pressed={active || isDrawLabel}
            title={
              hasScore
                ? `${value} ${percent(score)}`
                : isDrawLabel
                  ? `${value} zum Zeichnen ausgewählt`
                  : value
            }
            disabled={props.disabled}
            onClick={() => {
              if (props.onDrawLabelChange) {
                props.onDrawLabelChange(value)
                return
              }

              props.onToggle(value)
            }}
            className={[
              labelTileClass(active || isDrawLabel),
              props.disabled ? 'cursor-not-allowed opacity-50' : '',
            ].join(' ')}
          >
            <Icon
              className={[
                'h-5 w-5 transition sm:h-6 sm:w-6',
                active
                  ? 'text-indigo-700 dark:text-indigo-100'
                  : 'text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200',
              ].join(' ')}
              aria-hidden="true"
            />

            <span className="line-clamp-2 max-w-full break-words">
              {item.text}
            </span>

            {hasScore ? (
              <span
                className={[
                  'mt-0.5 rounded-full px-1.5 py-0.5 text-[9px] font-bold ring-1',
                  scoreDetectionPillClass(score),
                ].join(' ')}
              >
                {percent(score)}
              </span>
            ) : isDrawLabel ? (
              <span className="mt-0.5 rounded-full bg-amber-50 px-1.5 py-0.5 text-[9px] font-bold text-amber-700 ring-1 ring-amber-200 dark:bg-amber-500/15 dark:text-amber-200 dark:ring-amber-400/30">
                zeichnen
              </span>
            ) : active ? (
              <span className="mt-0.5 rounded-full bg-indigo-50 px-1.5 py-0.5 text-[9px] font-bold text-indigo-700 ring-1 ring-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-200 dark:ring-indigo-400/30">
                aktiv
              </span>
            ) : null}
          </button>
        )
      })}
    </div>
  )
}

function DetectorBoxLabelSelect(props: {
  values: string[]
  value: string
  disabled?: boolean
  compact?: boolean
  onChange: (value: string) => void
}) {
  const [open, setOpen] = useState(false)
  const [menuStyle, setMenuStyle] = useState<CSSProperties>({})
  const buttonRef = useRef<HTMLButtonElement | null>(null)

  const selectedItem = getSegmentLabelItem(props.value)
  const SelectedIcon = selectedItem.icon

  const close = useCallback(() => {
    setOpen(false)
  }, [])

  const updateMenuPosition = useCallback(() => {
    const button = buttonRef.current
    if (!button) return

    const rect = button.getBoundingClientRect()
    const viewportH = window.innerHeight
    const viewportW = window.innerWidth
    const spaceBelow = viewportH - rect.bottom
    const spaceAbove = rect.top
    const openUp = spaceBelow < 180 && spaceAbove > spaceBelow
    const maxHeight = openUp
      ? Math.min(260, Math.max(140, spaceAbove - 12))
      : Math.min(260, Math.max(140, spaceBelow - 12))

    setMenuStyle({
      position: 'fixed',
      left: Math.max(8, Math.min(rect.left, viewportW - rect.width - 8)),
      top: openUp ? undefined : rect.bottom + 4,
      bottom: openUp ? viewportH - rect.top + 4 : undefined,
      width: rect.width,
      maxHeight,
      zIndex: 2147483647,
    })
  }, [])

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

    updateMenuPosition()

    const onPointerDown = (e: PointerEvent) => {
      const target = e.target as HTMLElement | null
      if (target?.closest('[data-detector-label-select="true"]')) return
      close()
    }

    const onUpdate = () => updateMenuPosition()

    window.addEventListener('pointerdown', onPointerDown)
    window.addEventListener('resize', onUpdate)
    window.addEventListener('scroll', onUpdate, true)

    return () => {
      window.removeEventListener('pointerdown', onPointerDown)
      window.removeEventListener('resize', onUpdate)
      window.removeEventListener('scroll', onUpdate, true)
    }
  }, [open, close, updateMenuPosition])

  if (props.values.length === 0) {
    return (
      <button
        type="button"
        disabled
        className="mt-2 flex h-9 w-full items-center rounded-md border border-gray-200 bg-white px-2 text-sm text-gray-400 disabled:cursor-not-allowed dark:border-white/10 dark:bg-gray-950 dark:text-gray-500"
      >
        Keine Box-Labels
      </button>
    )
  }

  return (
    <div
      className={props.compact ? 'mt-1.5' : 'mt-2'}
      data-detector-label-select="true"
      onPointerDown={(e) => e.stopPropagation()}
      onClick={(e) => e.stopPropagation()}
    >
      <button
        ref={buttonRef}
        type="button"
        disabled={props.disabled}
        onClick={() => {
          if (props.disabled) return
          updateMenuPosition()
          setOpen((value) => !value)
        }}
        className={[
          'flex w-full items-center justify-between gap-2 rounded-xl border px-2.5 text-gray-900 shadow-sm outline-none transition',
          'focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 disabled:cursor-not-allowed disabled:opacity-50',
          'border-gray-200 bg-white',
          'dark:border-white/10 dark:bg-white/[0.05] dark:text-gray-100 dark:hover:bg-white/[0.07]',
          props.compact ? 'h-8 text-xs' : 'h-9 text-sm',
        ].join(' ')}
        aria-haspopup="listbox"
        aria-expanded={open}
      >
        <span className="flex min-w-0 items-center gap-2">
          <SelectedIcon
            className="h-4 w-4 shrink-0 text-gray-600 dark:text-gray-300"
            aria-hidden="true"
          />

          <span className="truncate">
            {selectedItem.text}
          </span>
        </span>

        <span
          className={[
            'shrink-0 text-sm text-gray-500 transition-transform dark:text-gray-400',
            open ? 'rotate-180' : '',
          ].join(' ')}
          aria-hidden="true"
        >
          ▾
        </span>
      </button>

      {open && typeof document !== 'undefined'
        ? createPortal(
            <div
              role="listbox"
              data-detector-label-select="true"
              style={menuStyle}
              className={[
                'overflow-y-auto rounded-xl border border-gray-200 bg-white py-1 shadow-2xl ring-1 ring-black/10',
                'dark:border-white/10 dark:bg-gray-900 dark:ring-white/15',
              ].join(' ')}
            >
              {props.values.map((value) => {
                const active = value === props.value
                const item = getSegmentLabelItem(value)
                const Icon = item.icon

                return (
                  <button
                    key={value}
                    type="button"
                    role="option"
                    aria-selected={active}
                    onClick={() => {
                      props.onChange(value)
                      setOpen(false)
                    }}
                    className={[
                      'flex w-full items-center gap-2 px-2 py-2 text-left text-sm transition',
                      active
                        ? 'bg-indigo-50 text-indigo-900 dark:bg-indigo-500/20 dark:text-indigo-100'
                        : 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-white/10 dark:hover:text-white',
                    ].join(' ')}
                  >
                    <Icon
                      className={[
                        'h-4 w-4 shrink-0',
                        active
                          ? 'text-indigo-700 dark:text-indigo-100'
                          : 'text-gray-600 dark:text-gray-300',
                      ].join(' ')}
                      aria-hidden="true"
                    />

                    <span className="min-w-0 flex-1 truncate">
                      {item.text}
                    </span>

                    <span className="shrink-0 text-[10px] text-gray-400">
                      {value}
                    </span>
                  </button>
                )
              })}
            </div>,
            document.body
          )
        : null}
    </div>
  )
}

function CollapsibleSingleLabelSection(props: {
  title: string
  values: string[]
  value: string
  score?: number
  predictionValue?: string
  expanded: boolean
  onExpandedChange: (expanded: boolean) => void
  onChange: (value: string) => void
  disabled?: boolean
  gridClassName?: string
}) {
  const currentValue = String(props.value || 'unknown').trim() || 'unknown'
  const selectedItem = getSegmentLabelItem(currentValue)
  const SelectedIcon = selectedItem.icon
  const shown = props.expanded
  const hasSelection = currentValue !== '' && currentValue !== 'unknown'

  return (
    <div className="rounded-lg bg-gray-50 p-2 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
      <button
        type="button"
        className="flex w-full items-center justify-between gap-2 text-left"
        onClick={() => props.onExpandedChange(!props.expanded)}
        aria-expanded={shown}
      >
        <div className="min-w-0">
          <div className="flex min-w-0 items-center gap-2">
            <SelectedIcon
              className={[
                'h-4 w-4 shrink-0',
                hasSelection
                  ? 'text-indigo-600 dark:text-indigo-300'
                  : 'text-gray-500 dark:text-gray-400',
              ].join(' ')}
              aria-hidden="true"
            />

            <div className="min-w-0 truncate text-xs font-medium text-gray-700 dark:text-gray-200">
              {props.title}
            </div>
          </div>

          <div className="mt-0.5 text-[10px] text-gray-500 dark:text-gray-400">
            {hasSelection
              ? selectedItem.text
              : shown
                ? 'Zum Einklappen klicken'
                : 'Zum Ausklappen klicken'}
          </div>
        </div>

        <div className="flex shrink-0 items-center gap-2">
          {typeof props.score === 'number' && Number.isFinite(props.score) ? (
            <span
              className={[
                'rounded-full px-1.5 py-0.5 text-[10px] font-bold ring-1',
                scoreDetectionPillClass(props.score),
              ].join(' ')}
              title={
                props.predictionValue
                  ? `Modell: ${props.predictionValue}`
                  : 'Modell-Vorhersage'
              }
            >
              {percent(props.score)}
            </span>
          ) : null}

          {hasSelection ? (
            <span className="rounded-full bg-indigo-100 px-2 py-0.5 text-[11px] font-semibold text-indigo-800 ring-1 ring-indigo-200 dark:bg-indigo-500/20 dark:text-indigo-100 dark:ring-indigo-300/30">
              aktiv
            </span>
          ) : null}

          <span
            className={[
              'text-sm text-gray-500 transition-transform dark:text-gray-400',
              shown ? 'rotate-180' : '',
            ].join(' ')}
            aria-hidden="true"
          >
            ▾
          </span>
        </div>
      </button>

      {shown ? (
        <div className="mt-2">
          {props.values.length === 0 ? (
            <div className="rounded-lg bg-gray-50 px-3 py-2 text-[11px] text-gray-500 ring-1 ring-black/5 dark:bg-white/5 dark:text-gray-400 dark:ring-white/10">
              Keine Einträge verfügbar.
            </div>
          ) : (
            <div className={props.gridClassName || 'grid grid-cols-2 gap-2'}>
              {props.values.map((value) => {
                const active = value === currentValue
                const item = getSegmentLabelItem(value)
                const Icon = item.icon
                const isPrediction = value === props.predictionValue

                return (
                  <button
                    key={value}
                    type="button"
                    aria-pressed={active}
                    title={value}
                    disabled={props.disabled}
                    onClick={() => props.onChange(value)}
                    className={[
                      labelTileClass(active),
                      props.disabled ? 'cursor-not-allowed opacity-50' : '',
                    ].join(' ')}
                  >
                    <Icon
                      className={[
                        'h-5 w-5 transition sm:h-6 sm:w-6',
                        active
                          ? 'text-indigo-700 dark:text-indigo-100'
                          : 'text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200',
                      ].join(' ')}
                      aria-hidden="true"
                    />

                    <span className="line-clamp-2 max-w-full break-words">
                      {item.text}
                    </span>

                    {active ? (
                      <span className="mt-0.5 rounded-full bg-indigo-50 px-1.5 py-0.5 text-[9px] font-bold text-indigo-700 ring-1 ring-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-200 dark:ring-indigo-400/30">
                        aktiv
                      </span>
                    ) : isPrediction && typeof props.score === 'number' ? (
                      <span
                        className={[
                          'mt-0.5 rounded-full px-1.5 py-0.5 text-[9px] font-bold ring-1',
                          scoreDetectionPillClass(props.score),
                        ].join(' ')}
                      >
                        Modell {percent(props.score)}
                      </span>
                    ) : null}
                  </button>
                )
              })}
            </div>
          )}
        </div>
      ) : null}
    </div>
  )
}

function CollapsibleLabelSection(props: {
  title: string
  values: string[]
  selected: string[]
  scores?: Record<string, number>
  expanded: boolean
  onExpandedChange: (expanded: boolean) => void
  onToggle: (value: string) => void
  drawLabel?: string
  onDrawLabelChange?: (value: string) => void
  disabled?: boolean
  singleDrawMode?: boolean
  gridClassName?: string
}) {
  const cleanDrawLabel = String(props.drawLabel || '').trim()
  const hasDrawLabelInSection =
    cleanDrawLabel !== '' && props.values.includes(cleanDrawLabel)

  const activeCount = props.singleDrawMode
    ? Math.max(props.selected.length, hasDrawLabelInSection ? 1 : 0)
    : props.selected.length

  const hasActiveItems = activeCount > 0
  const shown = props.expanded

  return (
    <div className="rounded-lg bg-gray-50 p-2 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
      <button
        type="button"
        className="flex w-full items-center justify-between gap-2 text-left"
        onClick={() => props.onExpandedChange(!props.expanded)}
        aria-expanded={shown}
      >
        <div className="min-w-0">
          <div className="text-xs font-medium text-gray-700 dark:text-gray-200">
            {props.title}
          </div>

          <div className="mt-0.5 text-[10px] text-gray-500 dark:text-gray-400">
            {hasActiveItems
              ? `${activeCount} aktiv`
              : shown
                ? 'Zum Einklappen klicken'
                : 'Zum Ausklappen klicken'}
          </div>
        </div>

        <div className="flex shrink-0 items-center gap-2">
          {hasActiveItems ? (
            <span className="rounded-full bg-indigo-100 px-2 py-0.5 text-[11px] font-semibold text-indigo-800 ring-1 ring-indigo-200 dark:bg-indigo-500/20 dark:text-indigo-100 dark:ring-indigo-300/30">
              {activeCount}
            </span>
          ) : null}

          <span
            className={[
              'text-sm text-gray-500 transition-transform dark:text-gray-400',
              shown ? 'rotate-180' : '',
            ].join(' ')}
            aria-hidden="true"
          >
            ▾
          </span>
        </div>
      </button>

      {shown ? (
        <div className="mt-2">
          <LabelToggleGrid
            values={props.values}
            selected={props.selected}
            scores={props.scores}
            onToggle={props.onToggle}
            drawLabel={props.drawLabel}
            onDrawLabelChange={props.onDrawLabelChange}
            disabled={props.disabled}
            gridClassName={props.gridClassName}
          />
        </div>
      ) : null}
    </div>
  )
}

function TrainingStatsList(props: {
  title: string
  description?: string
  values: TrainingLabelStat[]
  total: number
  confidence?: TrainingConfidence
  emptyText?: string
}) {
  const sorted = [...props.values].sort((a, b) => b.count - a.count)

  return (
    <div className="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/70">
      <div className="border-b border-gray-200 bg-gray-50/80 px-4 py-3 dark:border-white/10 dark:bg-white/[0.03]">
        <div className="flex items-start justify-between gap-3">
          <div className="min-w-0">
            <div className="text-sm font-semibold text-gray-900 dark:text-white">
              {props.title}
            </div>

            {props.description ? (
              <div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
                {props.description}
              </div>
            ) : null}
          </div>

          <div className="flex shrink-0 items-center gap-2">
            <span
              className={[
                'rounded-full px-2.5 py-1 text-xs font-bold ring-1',
                confidencePillClass(props.confidence),
              ].join(' ')}
              title={`Kategorie-Confidence: ${confidencePercent(props.confidence)}`}
            >
              {confidenceLabel(props.confidence)} · {confidencePercent(props.confidence)}
            </span>

            <span className="rounded-full bg-white px-2.5 py-1 text-xs font-semibold text-gray-700 ring-1 ring-gray-200 dark:bg-gray-950 dark:text-gray-200 dark:ring-white/10">
              {sorted.length} Labels
            </span>
          </div>
        </div>

        <div className="mt-3 flex flex-wrap items-center gap-2 text-[11px] text-gray-500 dark:text-gray-400">
          <span className="inline-flex items-center gap-1">
            <span className="h-1.5 w-5 rounded-full bg-indigo-500" />
            Häufigkeit
          </span>

          <span className="inline-flex items-center gap-1">
            <span className="h-1.5 w-5 rounded-full bg-emerald-500" />
            Confidence
          </span>
        </div>
      </div>

      {sorted.length === 0 ? (
        <div className="flex min-h-40 items-center justify-center px-4 py-8 text-center">
          <div>
            <div className="text-sm font-medium text-gray-700 dark:text-gray-200">
              Keine Labels gefunden
            </div>

            <div className="mt-1 max-w-sm text-xs text-gray-500 dark:text-gray-400">
              {props.emptyText || 'Sobald Feedback für diese Kategorie gespeichert wurde, erscheinen hier die Werte.'}
            </div>
          </div>
        </div>
      ) : (
        <div className="divide-y divide-gray-100 dark:divide-white/10">
          {sorted.map((item) => {
            const labelItem = getSegmentLabelItem(item.label)
            const Icon = labelItem.icon

            const shareWidth = countPercent(item.count, props.total)
            const confWidth = confidencePercent(item.confidence)

            return (
              <div
                key={item.label}
                className="px-4 py-3 transition hover:bg-gray-50 dark:hover:bg-white/[0.03]"
              >
                <div className="flex items-start justify-between gap-3">
                  <div className="flex min-w-0 items-start gap-3">
                    <div className="mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gray-100 ring-1 ring-gray-200 dark:bg-white/10 dark:ring-white/10">
                      <Icon
                        className="h-5 w-5 text-gray-600 dark:text-gray-300"
                        aria-hidden="true"
                      />
                    </div>

                    <div className="min-w-0">
                      <div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
                        {labelItem.text}
                      </div>

                      <div className="mt-0.5 truncate text-[11px] text-gray-500 dark:text-gray-400">
                        {item.label}
                      </div>
                    </div>
                  </div>

                  <div className="flex shrink-0 items-center gap-2">
                    <span
                      className={[
                        'rounded-full px-2 py-0.5 text-[11px] font-bold ring-1',
                        confidencePillClass(item.confidence),
                      ].join(' ')}
                      title={`Confidence: ${confidencePercent(item.confidence)}`}
                    >
                      {confidenceLabel(item.confidence)}
                    </span>

                    <div className="min-w-10 text-right text-sm font-bold text-gray-900 dark:text-white">
                      {item.count}
                    </div>
                  </div>
                </div>

                <div className="mt-3 space-y-1.5">
                  <div>
                    <div className="mb-1 flex items-center justify-between text-[10px] text-gray-500 dark:text-gray-400">
                      <span>Häufigkeit in dieser Gruppe</span>
                      <span>{shareWidth}</span>
                    </div>

                    <div className="h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-white/10">
                      <div
                        className="h-full rounded-full bg-indigo-500 transition-all"
                        style={{ width: shareWidth }}
                      />
                    </div>
                  </div>

                  <div>
                    <div className="mb-1 flex items-center justify-between text-[10px] text-gray-500 dark:text-gray-400">
                      <span>Daten-Confidence</span>
                      <span>{confWidth}</span>
                    </div>

                    <div className="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-white/10">
                      <div
                        className="h-full rounded-full bg-emerald-500 transition-all"
                        style={{ width: confWidth }}
                      />
                    </div>
                  </div>
                </div>
              </div>
            )
          })}
        </div>
      )}
    </div>
  )
}

type TrainingStatsTabKey =
  | 'people'
  | 'sexPositions'
  | 'bodyParts'
  | 'objects'
  | 'clothing'

function TrainingStatsModal(props: {
  open: boolean
  onClose: () => void
  stats: TrainingStats | null
  loading: boolean
  error: string | null
  feedbackCount: number
  requiredCount: number
}) {
  const [activeTab, setActiveTab] = useState<TrainingStatsTabKey>('people')
  const stats = props.stats

  const acceptedCount = stats?.acceptedCount ?? 0
  const correctedCount = stats?.correctedCount ?? 0
  const totalFeedback = stats?.feedbackCount ?? props.feedbackCount
  const boxCount = stats?.boxCount ?? 0
  const sampleCount = stats?.sampleCount ?? 0
  const overallConfidence = stats?.confidence

  const tabItems: Array<{
    key: TrainingStatsTabKey
    title: string
    shortTitle: string
    description: string
    values: TrainingLabelStat[]
    total: number
    confidence?: TrainingConfidence
  }> = [
    {
      key: 'people',
      title: 'Personen',
      shortTitle: 'Personen',
      description: 'Personen- und Gender-Labels aus Boxen.',
      values: stats?.labels.people ?? [],
      total: Math.max(1, boxCount),
      confidence: averageCategoryConfidence(stats?.labels.people ?? []),
    },
    {
      key: 'sexPositions',
      title: 'Sexpositionen',
      shortTitle: 'Positionen',
      description: 'Positions-Labels aus YOLO-Detector-Labels pro bewertetem Frame.',
      values: stats?.labels.sexPositions ?? [],
      total: Math.max(1, totalFeedback),
      confidence: averageCategoryConfidence(stats?.labels.sexPositions ?? []),
    },
    {
      key: 'bodyParts',
      title: 'Körperteile',
      shortTitle: 'Körper',
      description: 'Körperteil-Labels aus Korrekturen und Boxen.',
      values: stats?.labels.bodyParts ?? [],
      total: Math.max(1, totalFeedback),
      confidence: averageCategoryConfidence(stats?.labels.bodyParts ?? []),
    },
    {
      key: 'objects',
      title: 'Gegenstände',
      shortTitle: 'Objekte',
      description: 'Objekt-Labels aus Korrekturen und Boxen.',
      values: stats?.labels.objects ?? [],
      total: Math.max(1, totalFeedback),
      confidence: averageCategoryConfidence(stats?.labels.objects ?? []),
    },
    {
      key: 'clothing',
      title: 'Kleidung',
      shortTitle: 'Kleidung',
      description: 'Kleidungs-Labels aus Korrekturen und Boxen.',
      values: stats?.labels.clothing ?? [],
      total: Math.max(1, totalFeedback),
      confidence: averageCategoryConfidence(stats?.labels.clothing ?? []),
    },
  ]

  const activeTabItem =
    tabItems.find((item) => item.key === activeTab) ?? tabItems[0]

  return (
    <Modal
      open={props.open}
      onClose={props.onClose}
      title="Training-Statistiken"
      width="max-w-3xl"
      bodyClassName="p-0"
    >
      <div className="px-3 pb-4 pt-2 sm:px-6 sm:pb-6 sm:pt-4">
        {props.loading ? (
          <div className="flex min-h-40 items-center justify-center">
            <div className="text-center">
              <LoadingSpinner
                size="lg"
                srLabel="Statistiken werden geladen…"
              />

              <div className="mt-3 text-sm font-medium text-gray-700 dark:text-gray-200">
                Statistiken werden geladen…
              </div>
            </div>
          </div>
        ) : props.error ? (
          <div className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-500/10 dark:text-red-200">
            {props.error}
          </div>
        ) : (
          <div className="space-y-3 sm:space-y-4">
            {/* Mobile: kompakte Top-Zusammenfassung */}
            <div className="sm:hidden">
              <div className="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900/70">
                <div className="flex items-start justify-between gap-3">
                  <div className="min-w-0">
                    <div className="text-sm font-semibold text-gray-900 dark:text-white">
                      {stats?.modelAvailable
                        ? 'Modell verfügbar'
                        : 'Noch kein Modell'}
                    </div>

                    <div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
                      {totalFeedback} Feedback · {boxCount} Boxen · {sampleCount} Samples
                    </div>
                  </div>

                  <span
                    className={[
                      'shrink-0 rounded-full px-2.5 py-1 text-xs font-bold ring-1',
                      confidencePillClass(overallConfidence),
                    ].join(' ')}
                    title={`Daten-Confidence: ${confidencePercent(overallConfidence)}`}
                  >
                    {confidencePercent(overallConfidence)}
                  </span>
                </div>

                <div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-200 dark:bg-white/10">
                  <div
                    className="h-full rounded-full bg-emerald-500 transition-all"
                    style={{ width: confidencePercent(overallConfidence) }}
                  />
                </div>

                <div className="mt-3 grid grid-cols-4 gap-1.5">
                  <div className="rounded-xl bg-gray-50 px-2 py-1.5 text-center ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                    <div className="text-[9px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
                      Feedback
                    </div>
                    <div className="mt-0.5 text-sm font-black text-gray-900 dark:text-white">
                      {totalFeedback}
                    </div>
                  </div>

                  <div className="rounded-xl bg-gray-50 px-2 py-1.5 text-center ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                    <div className="text-[9px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
                      Passt
                    </div>
                    <div className="mt-0.5 text-sm font-black text-emerald-700 dark:text-emerald-300">
                      {acceptedCount}
                    </div>
                  </div>

                  <div className="rounded-xl bg-gray-50 px-2 py-1.5 text-center ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                    <div className="text-[9px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
                      Korr.
                    </div>
                    <div className="mt-0.5 text-sm font-black text-amber-700 dark:text-amber-300">
                      {correctedCount}
                    </div>
                  </div>

                  <div className="rounded-xl bg-gray-50 px-2 py-1.5 text-center ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                    <div className="text-[9px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
                      Boxen
                    </div>
                    <div className="mt-0.5 text-sm font-black text-gray-900 dark:text-white">
                      {boxCount}
                    </div>
                  </div>
                </div>
              </div>
            </div>

            {/* Desktop/Tablet: ausführliche Karten */}
            <div className="hidden sm:grid sm:grid-cols-4 sm:gap-2">
              <div className="rounded-xl bg-gray-50 p-3 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                <div className="text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
                  Feedback
                </div>
                <div className="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
                  {totalFeedback}
                </div>
                <div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
                  benötigt: {props.requiredCount}
                </div>
              </div>

              <div className="rounded-xl bg-gray-50 p-3 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                <div className="text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
                  Passt so
                </div>
                <div className="mt-1 text-2xl font-bold text-emerald-700 dark:text-emerald-300">
                  {acceptedCount}
                </div>
                <div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
                  {countPercent(acceptedCount, totalFeedback)}
                </div>
              </div>

              <div className="rounded-xl bg-gray-50 p-3 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                <div className="text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
                  Korrigiert
                </div>
                <div className="mt-1 text-2xl font-bold text-amber-700 dark:text-amber-300">
                  {correctedCount}
                </div>
                <div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
                  {countPercent(correctedCount, totalFeedback)}
                </div>
              </div>

              <div className="rounded-xl bg-gray-50 p-3 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                <div className="text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
                  Boxen
                </div>
                <div className="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
                  {boxCount}
                </div>
                <div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
                  {sampleCount} Samples
                </div>
              </div>
            </div>

            <div className="hidden grid-cols-1 gap-3 sm:grid sm:grid-cols-2">
              <div className="rounded-xl border border-indigo-100 bg-indigo-50 p-4 dark:border-indigo-400/20 dark:bg-indigo-500/10">
                <div className="text-[11px] font-medium uppercase tracking-wide text-indigo-700/80 dark:text-indigo-200/80">
                  Modellstatus
                </div>

                <div className="mt-2 text-sm font-semibold text-indigo-950 dark:text-indigo-50">
                  {stats?.modelAvailable
                    ? 'Trainiertes Modell verfügbar'
                    : 'Noch kein trainiertes Modell verfügbar'}
                </div>

                <div className="mt-1 text-xs leading-relaxed text-indigo-800/80 dark:text-indigo-100/70">
                  {stats?.modelAvailable
                    ? 'Die aktuellen Trainingsdaten können bereits von einem Modell genutzt werden.'
                    : 'Sammle weiter Feedback und starte anschließend das Training.'}
                </div>
              </div>

              <div className="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900/70">
                <div className="flex items-center justify-between gap-3">
                  <div>
                    <div className="text-[11px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
                      Daten-Confidence
                    </div>

                    <div className="mt-2 text-xl font-bold text-gray-900 dark:text-white">
                      {confidenceLabel(overallConfidence)}
                    </div>
                  </div>

                  <span
                    className={[
                      'rounded-full px-3 py-1.5 text-sm font-bold ring-1',
                      confidencePillClass(overallConfidence),
                    ].join(' ')}
                  >
                    {confidencePercent(overallConfidence)}
                  </span>
                </div>

                <div className="mt-3 h-2.5 overflow-hidden rounded-full bg-gray-200 dark:bg-white/10">
                  <div
                    className="h-full rounded-full bg-emerald-500 transition-all"
                    style={{ width: confidencePercent(overallConfidence) }}
                  />
                </div>

                <div className="mt-2 text-xs leading-relaxed text-gray-500 dark:text-gray-400">
                  Daten-Confidence aus Feedback-Menge, Boxen, Label-Abdeckung und Korrekturanteil. Kein direkter Modell-Qualitätswert.
                </div>
              </div>
            </div>

            {/* Mobile: Tabs kompakter, direkt nach Summary */}
            <div className="overflow-hidden rounded-xl border border-gray-200 bg-gray-50/70 p-1.5 dark:border-white/10 dark:bg-white/[0.03] sm:p-2">
              <div className="grid grid-cols-2 gap-1 sm:grid-cols-5">
                {tabItems.map((item) => {
                  const active = item.key === activeTab
                  const count = item.values.length

                  return (
                    <button
                      key={item.key}
                      type="button"
                      onClick={() => setActiveTab(item.key)}
                      className={[
                        'rounded-xl px-2.5 py-2 text-left transition',
                        'focus:outline-none focus:ring-2 focus:ring-indigo-500/40',
                        active
                          ? 'bg-white text-indigo-700 shadow-sm ring-1 ring-indigo-200 dark:bg-gray-950 dark:text-indigo-200 dark:ring-indigo-400/30'
                          : 'text-gray-600 hover:bg-white/70 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-white/10 dark:hover:text-white',
                      ].join(' ')}
                    >
                      <div className="truncate text-xs font-semibold">
                        <span className="sm:hidden">{item.shortTitle}</span>
                        <span className="hidden sm:inline">{item.title}</span>
                      </div>

                      <div className="mt-1 flex items-center justify-between gap-2">
                        <span className="text-[10px] opacity-70">
                          {count} Labels
                        </span>

                        <span
                          className={[
                            'rounded-full px-1.5 py-0.5 text-[9px] font-bold ring-1',
                            confidencePillClass(item.confidence),
                          ].join(' ')}
                          title={`Kategorie-Confidence: ${confidencePercent(item.confidence)}`}
                        >
                          {confidencePercent(item.confidence)}
                        </span>
                      </div>
                    </button>
                  )
                })}
              </div>
            </div>

            <TrainingStatsList
              title={activeTabItem.title}
              description={activeTabItem.description}
              values={activeTabItem.values}
              total={activeTabItem.total}
              confidence={activeTabItem.confidence}
            />
          </div>
        )}
      </div>
    </Modal>
  )
}

export default function TrainingTab(props: {
  onTrainingRunningChange?: (running: boolean) => void
}) {
  const [labels, setLabels] = useState<TrainingLabels>(emptyLabels)
  const [sample, setSample] = useState<TrainingSample | null>(null)
  const [correction, setCorrection] = useState<CorrectionState>(() => predictionToCorrection(null))
  const [hasManualCorrection, setHasManualCorrection] = useState(false)
  const [loading, setLoading] = useState(false)
  const [analysisProgress, setAnalysisProgress] = useState(0)
  const [analysisStep, setAnalysisStep] = useState('')
  const [saving, setSaving] = useState(false)
  const [training, setTraining] = useState(false)
  const [trainingStatus, setTrainingStatus] = useState<TrainingStatus | null>(null)
  const [deletingTrainingData, setDeletingTrainingData] = useState(false)
  const [trainingProgress, setTrainingProgress] = useState(0)
  const [trainingStep, setTrainingStep] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [message, setMessage] = useState<string | null>(null)
  const [statsModalOpen, setStatsModalOpen] = useState(false)
  const [cancellingTraining, setCancellingTraining] = useState(false)
  const [trainingStats, setTrainingStats] = useState<TrainingStats | null>(null)
  const [trainingStatsLoading, setTrainingStatsLoading] = useState(false)
  const [trainingStatsError, setTrainingStatsError] = useState<string | null>(null)
  const wasTrainingRunningRef = useRef(false)
  const shownTrainingCompletionRef = useRef<string | null>(null)

  const [frameImageLoaded, setFrameImageLoaded] = useState(false)

  const imageBoxRef = useRef<HTMLDivElement | null>(null)

  const detectorBoxesScrollRef = useRef<HTMLDivElement | null>(null)
  const detectorBoxItemRefs = useRef<Array<HTMLDivElement | null>>([])

  const epochTimingRef = useRef<{
    firstEpochAt: number
    lastEpoch: number
    lastAt: number
  }>({
    firstEpochAt: 0,
    lastEpoch: 0,
    lastAt: 0,
  })

  const etaSmoothingRef = useRef<{
    lastAt: number
  }>({
    lastAt: 0,
  })

  const [trainingNowMs, setTrainingNowMs] = useState(() => Date.now())
  const [smoothedTrainingEtaMs, setSmoothedTrainingEtaMs] = useState(0)

  const [estimatedEpochMs, setEstimatedEpochMs] = useState(0)

  const [drawingBox, setDrawingBox] = useState<DrawingTrainingBox | null>(null)
  const [boxInteraction, setBoxInteraction] = useState<BoxInteraction | null>(null)
  const [touchMagnifier, setTouchMagnifier] = useState<MagnifierState | null>(null)
  const [boxLabel, setBoxLabel] = useState('')
  const [activeBoxIndex, setActiveBoxIndex] = useState<number | null>(null)
  const [imageReloadKey, setImageReloadKey] = useState(0)
  const [expandedCorrectionSections, setExpandedCorrectionSections] = useState({
    sexPosition: false,
    people: false,
    bodyParts: false,
    objects: false,
    clothing: false,
  })
  
  const [mobilePanel, setMobilePanel] = useState<'labels' | 'boxes' | 'training'>('labels')

  const mobileLabelsScrollRef = useRef<HTMLDivElement | null>(null)

  const mobileSectionRefs = useRef<
    Record<keyof typeof expandedCorrectionSections, HTMLDivElement | null>
  >({
    sexPosition: null,
    people: null,
    bodyParts: null,
    objects: null,
    clothing: null,
  })

  const scrollMobileSectionToTop = useCallback(
    (key: keyof typeof expandedCorrectionSections) => {
      window.requestAnimationFrame(() => {
        const scrollEl = mobileLabelsScrollRef.current
        const sectionEl = mobileSectionRefs.current[key]

        if (!scrollEl || !sectionEl) return

        const scrollRect = scrollEl.getBoundingClientRect()
        const sectionRect = sectionEl.getBoundingClientRect()

        scrollEl.scrollTo({
          top: scrollEl.scrollTop + sectionRect.top - scrollRect.top,
          behavior: 'smooth',
        })
      })
    },
    []
  )

  const toggleMobileCorrectionSection = useCallback(
    (key: keyof typeof expandedCorrectionSections, expanded: boolean) => {
      setExpandedCorrectionSections((p) => ({
        ...p,
        [key]: expanded,
      }))

      if (expanded && mobilePanel === 'labels') {
        scrollMobileSectionToTop(key)
      }
    },
    [mobilePanel, scrollMobileSectionToTop]
  )

  const labelsRef = useRef<TrainingLabels>(emptyLabels)

  useEffect(() => {
    labelsRef.current = labels
  }, [labels])

  const boxLabels = useMemo(() => {
    return uniqStrings([
      ...labels.people,
      ...labels.bodyParts,
      ...labels.objects,
      ...labels.clothing,
    ]).sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
  }, [labels.people, labels.bodyParts, labels.objects, labels.clothing])

  const correctionBoxes = correction.boxes ?? []

  const visibleBoxes = [
    ...correctionBoxes.map((box, index) => ({ box, index, isDraft: false })),
    ...(drawingBox
      ? [{ box: drawingBox, index: -1, isDraft: true }]
      : []),
  ]

  const bodyPartScores = useMemo(() => {
    return Object.fromEntries(
      (sample?.prediction.bodyPartsPresent ?? [])
        .filter((x) => x.label)
        .map((x) => [x.label, x.score])
    )
  }, [sample?.prediction.bodyPartsPresent])

  const objectScores = useMemo(() => {
    return Object.fromEntries(
      (sample?.prediction.objectsPresent ?? [])
        .filter((x) => x.label)
        .map((x) => [x.label, x.score])
    )
  }, [sample?.prediction.objectsPresent])

  const clothingScores = useMemo(() => {
    return Object.fromEntries(
      (sample?.prediction.clothingPresent ?? [])
        .filter((x) => x.label)
        .map((x) => [x.label, x.score])
    )
  }, [sample?.prediction.clothingPresent])

  const peopleScores = useMemo(() => {
    const bestScores = new Map<string, number>()

    for (const box of sample?.prediction.boxes ?? []) {
      const cleanLabel = String(box.label || '').trim()
      const n = Number(box.score)

      if (!cleanLabel || !labels.people.includes(cleanLabel)) continue
      if (!Number.isFinite(n)) continue

      const current = bestScores.get(cleanLabel)
      if (current === undefined || n > current) {
        bestScores.set(cleanLabel, n)
      }
    }

    return Object.fromEntries(bestScores)
  }, [sample?.prediction.boxes, labels.people])

  const selectedPeopleLabels = useMemo(() => {
    return correction.peoplePresent
  }, [correction.peoplePresent])

  const drawLabelForSection = useCallback((values: string[]) => {
    const clean = String(boxLabel || '').trim()
    return values.includes(clean) ? clean : ''
  }, [boxLabel])

  const imageSrc = useMemo(() => {
    if (!sample?.frameUrl) return ''

    return `${sample.frameUrl}&t=${encodeURIComponent(sample.sampleId)}&r=${imageReloadKey}`
  }, [sample, imageReloadKey])

  useEffect(() => {
    if (!imageSrc) {
      setFrameImageLoaded(false)
      return
    }

    setFrameImageLoaded(false)
  }, [imageSrc])

  const canStartTraining = Boolean(trainingStatus?.canTrain)
  const feedbackCount = trainingStatus?.feedbackCount ?? 0
  const requiredCount = trainingStatus?.requiredCount ?? 5

  const trainingRunning = training || Boolean(trainingStatus?.training?.running)
  const uiLocked = loading || saving || trainingRunning || deletingTrainingData
  const shownTrainingProgress = trainingRunning
    ? trainingStatus?.training?.progress ?? trainingProgress
    : trainingProgress
  const shownTrainingStep = trainingRunning
    ? trainingStatus?.training?.step || trainingStep || 'Training läuft…'
    : trainingStep
  const analysisConfidence = useMemo(() => {
    return currentAnalysisConfidence(sample?.prediction)
  }, [sample?.prediction])

  const loadLabels = useCallback(async () => {
    const res = await fetch('/api/training/labels', { cache: 'no-store' })
    if (!res.ok) return

    const data = await res.json().catch(() => null)
    if (data) setLabels(sortTrainingLabels(data))
  }, [])

  const applyTrainingStatus = useCallback((data: any) => {
    if (!data) return

    const job = data.training || null

    setTrainingStatus((prev) => ({
      feedbackCount: Number(data.feedbackCount ?? prev?.feedbackCount ?? 0),
      requiredCount: Number(data.requiredCount ?? prev?.requiredCount ?? 5),
      canTrain: Boolean(data.canTrain ?? prev?.canTrain ?? false),

      detector: data.detector
        ? {
            trainCount: Number(data.detector.trainCount ?? 0),
            valCount: Number(data.detector.valCount ?? 0),
            requiredTrain: Number(data.detector.requiredTrain ?? 20),
            requiredVal: Number(data.detector.requiredVal ?? 3),
            datasetReady: Boolean(data.detector.datasetReady),
            dataReady: Boolean(data.detector.dataReady),
            modelExists: Boolean(data.detector.modelExists),
            modelPath: data.detector.modelPath,
            source: data.detector.source,
          }
        : prev?.detector,

      training: job
        ? {
            running: Boolean(job.running),
            progress: Number(job.progress ?? 0),
            step: String(job.step ?? ''),
            message: job.message,
            error: job.error,
            startedAt: job.startedAt,
            finishedAt: job.finishedAt,
            durationMs: Number(job.durationMs ?? 0),
            stage: job.stage,
            epoch: Number(job.epoch ?? 0),
            epochs: Number(job.epochs ?? 0),
          }
        : prev?.training,
    }))

    setTraining(Boolean(job?.running))

    if (job?.message && !job.running) {
      setTrainingProgress(100)
      setTrainingStep('Training abgeschlossen.')

      const finishedAt = String(job.finishedAt || '').trim()
      const completionKey =
        finishedAt || `${String(job.startedAt || '')}:${String(job.message || '')}`

      if (completionKey && shownTrainingCompletionRef.current !== completionKey) {
        shownTrainingCompletionRef.current = completionKey

        const duration = trainingDurationMs(job)
        const durationText = duration > 0
          ? ` Dauer: ${formatDuration(duration)}.`
          : ''

        setMessage(`${String(job.message)}${durationText}`)
      }
    }

    if (job?.error && !job.running) {
      const finishedAt = String(job.finishedAt || '').trim()
      const errorKey = finishedAt
        ? `${finishedAt}:error`
        : `${String(job.startedAt || '')}:error`

      if (errorKey && shownTrainingCompletionRef.current !== errorKey) {
        shownTrainingCompletionRef.current = errorKey
        setError(String(job.error))
      }
    }
  }, [])

  const loadNext = useCallback(async (opts?: {
    forceNew?: boolean
    refreshPrediction?: boolean
    preserveNotice?: boolean
  }) => {
    setLoading(true)
    setAnalysisProgress(8)
    setAnalysisStep(
      opts?.refreshPrediction
        ? 'Aktuelles Bild wird neu analysiert…'
        : opts?.forceNew
          ? 'Neues Trainingsbild wird gesucht…'
          : 'Trainingsbild wird geladen…'
    )

    if (!opts?.preserveNotice) {
      setError(null)
      setMessage(null)
    }

    try {
      const params = new URLSearchParams()

      if (opts?.forceNew) params.set('force', '1')
      if (opts?.refreshPrediction) params.set('refresh', '1')

      const url = `/api/training/next${params.toString() ? `?${params.toString()}` : ''}`

      setAnalysisProgress(25)
      setAnalysisStep('Bild wird vorbereitet…')

      const res = await fetch(url, { cache: 'no-store' })
      const data = await res.json().catch(() => null)

      if (!res.ok) {
        throw new Error(data?.error || `HTTP ${res.status}`)
      }

      setAnalysisProgress(92)
      setAnalysisStep('Analyse-Ergebnis wird übernommen…')

      const nextCorrection = predictionToCorrection(data)

      setDrawingBox(null)
      setBoxInteraction(null)
      setTouchMagnifier(null)
      setBoxLabel('')
      setActiveBoxIndex(null)
      setMobilePanel('labels')

      window.requestAnimationFrame(() => {
        mobileLabelsScrollRef.current?.scrollTo({
          top: 0,
          behavior: 'smooth',
        })
      })

      setSample(data)
      setCorrection(nextCorrection)
      setHasManualCorrection(false)

      setExpandedCorrectionSections({
        sexPosition:
          Boolean(nextCorrection.sexPosition) &&
          nextCorrection.sexPosition !== 'unknown',
        people: nextCorrection.peoplePresent.length > 0,
        bodyParts: nextCorrection.bodyPartsPresent.length > 0,
        objects: nextCorrection.objectsPresent.length > 0,
        clothing: nextCorrection.clothingPresent.length > 0,
      })
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setAnalysisProgress((value) => Math.max(value, 100))
      setAnalysisStep((value) => value || 'Analyse abgeschlossen.')

      window.setTimeout(() => {
        setLoading(false)
        setAnalysisProgress(0)
        setAnalysisStep('')
      }, 500)
    }
  }, [])

  const reloadCurrentImage = useCallback(async () => {
    setDrawingBox(null)
    setBoxInteraction(null)
    setTouchMagnifier(null)
    setActiveBoxIndex(null)

    await loadNext({ refreshPrediction: true })
    setImageReloadKey((value) => value + 1)
  }, [loadNext])

  const loadTrainingStatus = useCallback(async () => {
    const res = await fetch('/api/training/status', { cache: 'no-store' })
    const data = await res.json().catch(() => null)

    if (!res.ok || !data) return

    applyTrainingStatus(data)
  }, [applyTrainingStatus])

  const loadTrainingStats = useCallback(async () => {
    setTrainingStatsLoading(true)
    setTrainingStatsError(null)

    try {
      const res = await fetch('/api/training/stats', { cache: 'no-store' })
      const data = await res.json().catch(() => null)

      if (!res.ok) {
        throw new Error(data?.error || `HTTP ${res.status}`)
      }

      setTrainingStats({
        feedbackCount: Number(data?.feedbackCount ?? 0),
        acceptedCount: Number(data?.acceptedCount ?? 0),
        correctedCount: Number(data?.correctedCount ?? 0),
        sampleCount: Number(data?.sampleCount ?? 0),
        boxCount: Number(data?.boxCount ?? 0),
        modelAvailable: Boolean(data?.modelAvailable),
        confidence: data?.confidence,
        labels: {
          people: Array.isArray(data?.labels?.people) ? data.labels.people : [],
          sexPositions: Array.isArray(data?.labels?.sexPositions) ? data.labels.sexPositions : [],
          bodyParts: Array.isArray(data?.labels?.bodyParts) ? data.labels.bodyParts : [],
          objects: Array.isArray(data?.labels?.objects) ? data.labels.objects : [],
          clothing: Array.isArray(data?.labels?.clothing) ? data.labels.clothing : [],
        },
      })
    } catch (e) {
      setTrainingStatsError(e instanceof Error ? e.message : String(e))
    } finally {
      setTrainingStatsLoading(false)
    }
  }, [])

  useEffect(() => {
    const es = new EventSource('/api/events/stream')

    const onTraining = (ev: MessageEvent) => {
      try {
        const data = JSON.parse(String(ev.data ?? 'null'))

        if (data?.type !== 'training_status') return

        applyTrainingStatus({
          training: data.training,
        })
      } catch {
        // ignore
      }
    }

    es.addEventListener('training', onTraining as EventListener)

    es.onerror = () => {
      // Optional: Polling-Fallback bleibt separat bestehen.
    }

    return () => {
      es.removeEventListener('training', onTraining as EventListener)
      es.close()
    }
  }, [applyTrainingStatus])

  useEffect(() => {
    const draggingBox = Boolean(drawingBox || boxInteraction)

    if (!draggingBox) return

    const previousUserSelect = document.body.style.userSelect
    const previousWebkitUserSelect = document.body.style.webkitUserSelect

    document.body.style.userSelect = 'none'
    document.body.style.webkitUserSelect = 'none'

    const clearSelection = () => {
      window.getSelection()?.removeAllRanges()
    }

    const preventSelection = (event: Event) => {
      event.preventDefault()
    }

    clearSelection()

    document.addEventListener('selectionchange', clearSelection)
    document.addEventListener('selectstart', preventSelection)

    return () => {
      document.body.style.userSelect = previousUserSelect
      document.body.style.webkitUserSelect = previousWebkitUserSelect

      document.removeEventListener('selectionchange', clearSelection)
      document.removeEventListener('selectstart', preventSelection)

      clearSelection()
    }
  }, [drawingBox, boxInteraction])

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

    void loadTrainingStats()
  }, [statsModalOpen, loadTrainingStats])

  const onTrainingRunningChange = props.onTrainingRunningChange

  useEffect(() => {
    onTrainingRunningChange?.(trainingRunning)
  }, [trainingRunning, onTrainingRunningChange])

  useEffect(() => {
    if (!trainingRunning) {
      etaSmoothingRef.current = {
        lastAt: 0,
      }

      setSmoothedTrainingEtaMs(0)
      return
    }

    setTrainingNowMs(Date.now())

    const timer = window.setInterval(() => {
      setTrainingNowMs(Date.now())
    }, 1000)

    return () => window.clearInterval(timer)
  }, [trainingRunning])

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

    const currentLabels = labelsRef.current
    const hasLoadedBoxLabels =
      currentLabels.people.length > 0 ||
      currentLabels.bodyParts.length > 0 ||
      currentLabels.objects.length > 0 ||
      currentLabels.clothing.length > 0

    // Wichtig: Während Labels noch laden oder kurz leer sind, Auswahl nicht löschen.
    if (!hasLoadedBoxLabels) return

    const stillExists = [
      ...currentLabels.people,
      ...currentLabels.bodyParts,
      ...currentLabels.objects,
      ...currentLabels.clothing,
    ].includes(boxLabel)

    if (!stillExists) {
      setBoxLabel('')
    }
  }, [boxLabel, labels])

  useEffect(() => {
    const wasRunning = wasTrainingRunningRef.current

    if (wasRunning && !trainingRunning && trainingStatus?.training?.finishedAt) {
      void loadNext({ refreshPrediction: true, preserveNotice: true })
    }

    wasTrainingRunningRef.current = trainingRunning
  }, [trainingRunning, trainingStatus?.training?.finishedAt, loadNext])

  useEffect(() => {
    if (activeBoxIndex === null) return

    const scrollEl = detectorBoxesScrollRef.current
    const itemEl = detectorBoxItemRefs.current[activeBoxIndex]

    if (!scrollEl || !itemEl) return

    itemEl.scrollIntoView({
      block: 'nearest',
      inline: 'nearest',
      behavior: 'smooth',
    })
  }, [activeBoxIndex])

  useEffect(() => {
    let cancelled = false

    async function init() {
      await loadLabels()
      await loadTrainingStatus()

      if (cancelled) return

      // Wichtig: Auch während laufendem Training wieder das aktuelle offene Sample laden,
      // damit nicht "Kein Bild geladen" angezeigt wird.
      await loadNext()
    }

    void init()

    return () => {
      cancelled = true
    }
  }, [loadLabels, loadNext, loadTrainingStatus])

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

    const onVisibilityChange = () => {
      if (!document.hidden) {
        void loadTrainingStatus()
      }
    }

    document.addEventListener('visibilitychange', onVisibilityChange)

    return () => {
      document.removeEventListener('visibilitychange', onVisibilityChange)
    }
  }, [loadTrainingStatus, trainingRunning])

  useEffect(() => {
    if (!trainingRunning) {
      const timer = window.setTimeout(() => {
        setTrainingProgress(0)
        setTrainingStep('')
      }, 800)

      return () => window.clearTimeout(timer)
    }

    const serverProgress = Number(trainingStatus?.training?.progress ?? 0)
    const serverStep = String(trainingStatus?.training?.step ?? '')

    setTrainingProgress(Number.isFinite(serverProgress) ? clampPercent(serverProgress) : 0)
    setTrainingStep(serverStep || 'Training läuft…')
  }, [
    trainingRunning,
    trainingStatus?.training?.progress,
    trainingStatus?.training?.step,
  ])

  useEffect(() => {
    const job = trainingStatus?.training
    const epoch = Number(job?.epoch ?? 0)
    const epochs = Number(job?.epochs ?? 0)

    if (!trainingRunning || epoch <= 0 || epochs <= 0) {
      epochTimingRef.current = {
        firstEpochAt: 0,
        lastEpoch: 0,
        lastAt: 0,
      }

      setEstimatedEpochMs(0)
      return
    }

    const now = Date.now()
    const previous = epochTimingRef.current

    const firstEpochAt =
      previous.firstEpochAt > 0
        ? previous.firstEpochAt
        : job?.startedAt && Number.isFinite(Date.parse(job.startedAt))
          ? Date.parse(job.startedAt)
          : now

    const safeEpoch = Math.max(1, Math.min(epochs, Math.floor(epoch)))
    const elapsedSinceStartMs = Math.max(0, now - firstEpochAt)

    // Direkt ab Epoche 1 eine erste Schätzung:
    // bisherige Laufzeit / aktuelle Epoche.
    const averageFromElapsed =
      elapsedSinceStartMs > 0 && safeEpoch > 0
        ? elapsedSinceStartMs / safeEpoch
        : 0

    if (Number.isFinite(averageFromElapsed) && averageFromElapsed > 0) {
      setEstimatedEpochMs((old) => {
        if (!Number.isFinite(old) || old <= 0) {
          return averageFromElapsed
        }

        const clampedMeasured = Math.max(
          old * 0.60,
          Math.min(old * 1.60, averageFromElapsed)
        )

        return old * 0.75 + clampedMeasured * 0.25
      })
    }

    epochTimingRef.current = {
      firstEpochAt,
      lastEpoch: safeEpoch,
      lastAt: now,
    }
  }, [
    trainingRunning,
    trainingStatus?.training?.epoch,
    trainingStatus?.training?.epochs,
    trainingStatus?.training?.startedAt,
    trainingNowMs,
  ])

  const saveFeedback = useCallback(
    async (accepted: boolean) => {
      if (!sample) return

      setSaving(true)
      setError(null)
      setMessage(null)

      try {
        const normalizedBoxes = (correction.boxes ?? [])
          .map(normalizeBox)
          .filter((box) => box.label && box.w > 0 && box.h > 0)

        const correctionPayload: CorrectionState = {
          ...correction,
          peoplePresent: peopleLabelsFromBoxes(normalizedBoxes, labelsRef.current),
          boxes: normalizedBoxes,
        }

        const payload = {
          sampleId: sample.sampleId,
          accepted,
          correction: accepted && correctionPayload.boxes.length === 0
            ? undefined
            : correctionPayload,
        }

        const res = await fetch('/api/training/feedback', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        })

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

        if (!res.ok) {
          throw new Error(backendText(data, `HTTP ${res.status}`))
        }

        setMessage(
          accepted
            ? 'Feedback gespeichert: Prediction wurde als korrekt übernommen.'
            : 'Korrektur gespeichert.'
        )

        await loadTrainingStatus()
        await loadNext({ preserveNotice: true })
      } catch (e) {
        setError(e instanceof Error ? e.message : String(e))
      } finally {
        setSaving(false)
      }
    },
    [sample, correction, loadNext, loadTrainingStatus]
  )

  const startTraining = useCallback(async () => {
    shownTrainingCompletionRef.current = null

    setTraining(true)
    setTrainingProgress(5)
    setTrainingStep('Training wird gestartet…')
    setError(null)
    setMessage(null)

    try {
      const res = await fetch('/api/training/train', {
        method: 'POST',
      })

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

      if (!res.ok) {
        throw new Error(backendText(data, `HTTP ${res.status}`))
      }

      await loadTrainingStatus()

      // WICHTIG:
      // Hier NICHT direkt loadNext() aufrufen.
      // Das Training läuft im Backend asynchron weiter.
    } catch (e) {
      setTraining(false)
      setTrainingProgress(0)
      setTrainingStep('')
      setError(e instanceof Error ? e.message : String(e))
    }
  }, [loadTrainingStatus])

  const cancelTraining = useCallback(async () => {
    const confirmed = window.confirm(
      'Training wirklich abbrechen? Temporäre Trainingsausgaben dieses Laufs werden gelöscht. Deine Feedbacks und Labels bleiben erhalten.'
    )

    if (!confirmed) return

    setCancellingTraining(true)
    setError(null)
    setMessage(null)
    setTrainingStep('Training wird abgebrochen…')

    try {
      const res = await fetch('/api/training/cancel', {
        method: 'POST',
      })

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

      if (!res.ok) {
        throw new Error(backendText(data, `HTTP ${res.status}`))
      }

      await loadTrainingStatus()
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setCancellingTraining(false)
    }
  }, [loadTrainingStatus])

  const deleteAllTrainingData = useCallback(async () => {
    const confirmed = window.confirm(
      'Wirklich alle Trainingsdaten löschen? Das entfernt Feedback, Frames, Samples und Detector-Daten. Diese Aktion kann nicht rückgängig gemacht werden.'
    )

    if (!confirmed) return

    setDeletingTrainingData(true)
    setError(null)
    setMessage(null)

    try {
      const res = await fetch('/api/training/delete-all', {
        method: 'DELETE',
      })

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

      if (!res.ok) {
        throw new Error(data?.error || `HTTP ${res.status}`)
      }

      setSample(null)
      setCorrection(predictionToCorrection(null))
      setTrainingStatus({
        feedbackCount: 0,
        requiredCount,
        canTrain: false,
      })

      setMessage(backendText(data, 'Alle Trainingsdaten wurden gelöscht.'))

      await loadTrainingStatus()
      await loadNext({ forceNew: true, preserveNotice: true })
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e))
    } finally {
      setDeletingTrainingData(false)
    }
  }, [loadNext, loadTrainingStatus, requiredCount])

  const getPointerPosInImage = useCallback((
    clientX: number,
    clientY: number,
    opts?: { clamp?: boolean }
  ) => {
    const el = imageBoxRef.current
    if (!el) return null

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

    const x = (clientX - rect.left) / rect.width
    const y = (clientY - rect.top) / rect.height

    if (opts?.clamp === false) {
      return { x, y }
    }

    return {
      x: clamp01(x),
      y: clamp01(y),
    }
  }, [])

  const startDrawBox = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
    if (!boxLabel) return
    if (uiLocked) return
    if (boxInteraction) return

    const target = e.target as HTMLElement | null
    if (target?.closest('[data-box-control="true"]')) return

    const pos = getPointerPosInImage(e.clientX, e.clientY)
    if (!pos) return

    e.preventDefault()
    e.stopPropagation()
    window.getSelection()?.removeAllRanges()

    try {
      e.currentTarget.setPointerCapture(e.pointerId)
    } catch {
      // Pointer wurde vom Browser bereits abgebrochen.
    }

    setTouchMagnifier({
      visible: true,
      clientX: e.clientX,
      clientY: e.clientY,
      imageX: pos.x,
      imageY: pos.y,
    })

    setDrawingBox({
      label: boxLabel,
      startX: pos.x,
      startY: pos.y,
      x: pos.x,
      y: pos.y,
      w: 0,
      h: 0,
    })
  }, [boxLabel, boxInteraction, getPointerPosInImage, uiLocked])

  const moveDrawBox = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
    if (drawingBox || boxInteraction) {
      e.preventDefault()
      e.stopPropagation()
    }

    const clampedPos = getPointerPosInImage(e.clientX, e.clientY)
    if (!clampedPos) return

    const pos =
      boxInteraction?.type === 'move'
        ? getPointerPosInImage(e.clientX, e.clientY, { clamp: false }) ?? clampedPos
        : clampedPos

    if (drawingBox || boxInteraction) {
      setTouchMagnifier({
        visible: true,
        clientX: e.clientX,
        clientY: e.clientY,
        imageX: clampedPos.x,
        imageY: clampedPos.y,
      })
    }

    if (boxInteraction) {
      const dx = pos.x - boxInteraction.startX
      const dy = pos.y - boxInteraction.startY
      const original = boxInteraction.original

      let nextBox: TrainingBox = original

      if (boxInteraction.type === 'move') {
        nextBox = normalizeMovedBox({
          ...original,
          x: original.x + dx,
          y: original.y + dy,
        })
      }

      if (boxInteraction.type === 'resize') {
        let x1 = original.x
        let y1 = original.y
        let x2 = original.x + original.w
        let y2 = original.y + original.h

        const pointerX = snap01(clampedPos.x)
        const pointerY = snap01(clampedPos.y)

        // Wichtig:
        // Beim Resize folgt die gezogene Ecke direkt dem Pointer.
        // Dadurch bleibt kein Grab-Offset übrig, wenn der Handle nicht exakt
        // auf der mathematischen Box-Ecke getroffen wurde.
        if (boxInteraction.handle.includes('n')) y1 = pointerY
        if (boxInteraction.handle.includes('s')) y2 = pointerY
        if (boxInteraction.handle.includes('w')) x1 = pointerX
        if (boxInteraction.handle.includes('e')) x2 = pointerX

        const left = Math.min(x1, x2)
        const top = Math.min(y1, y2)
        const right = Math.max(x1, x2)
        const bottom = Math.max(y1, y2)

        nextBox = normalizeBox({
          ...original,
          x: left,
          y: top,
          w: right - left,
          h: bottom - top,
        })
      }

      const geometryChanged = boxGeometryChanged(original, nextBox)

      const correctedNextBox = geometryChanged
        ? markBoxCorrected(nextBox)
        : nextBox

      if (geometryChanged) {
        setHasManualCorrection(true)
      }

      setCorrection((prev) => ({
        ...prev,
        boxes: (prev.boxes ?? []).map((box, index) =>
          index === boxInteraction.index ? correctedNextBox : box
        ),
      }))

      return
    }

    if (!drawingBox) return

    const x1 = drawingBox.startX
    const y1 = drawingBox.startY
    const x2 = pos.x
    const y2 = pos.y

    setDrawingBox({
      ...drawingBox,
      x: Math.min(x1, x2),
      y: Math.min(y1, y2),
      w: Math.abs(x2 - x1),
      h: Math.abs(y2 - y1),
    })
  }, [boxInteraction, drawingBox, getPointerPosInImage])

  const finishDrawBox = useCallback((e?: React.PointerEvent<HTMLDivElement>) => {
    setTouchMagnifier(null)

    if (drawingBox || boxInteraction) {
      e?.preventDefault()
      e?.stopPropagation()
    }

    if (boxInteraction) {
      setBoxInteraction(null)
      return
    }

    if (!drawingBox) return

    const { startX, startY, ...drawingTrainingBox } = drawingBox
    const box = normalizeBox(drawingTrainingBox)

    setDrawingBox(null)

    if (box.w < 0.01 || box.h < 0.01) return

    setHasManualCorrection(true)

    setCorrection((prev) => {
      const previousBoxes = prev.boxes ?? []
      const newBoxIndex = previousBoxes.length

      const next: CorrectionState = {
        ...prev,
        boxes: [...previousBoxes, box],
      }

      setActiveBoxIndex(newBoxIndex)

      return applyBoxLabelToCorrection(next, box.label, labelsRef.current)
    })
  }, [boxInteraction, drawingBox])

  const removeBox = useCallback((index: number) => {
    setHasManualCorrection(true)

    let removedLabel = ''
    let shouldClearBoxLabel = false

    setCorrection((prev) => {
      const removed = prev.boxes?.[index]
      removedLabel = String(removed?.label || '').trim()

      const next = removeBoxFromCorrection(prev, index, labelsRef.current)

      if (removedLabel) {
        shouldClearBoxLabel = !next.boxes.some(
          (box) => String(box.label || '').trim() === removedLabel
        )
      }

      return next
    })

    if (removedLabel && shouldClearBoxLabel) {
      setBoxLabel((current) => current === removedLabel ? '' : current)
    }

    setActiveBoxIndex((current) => {
      if (current === null) return null
      if (current === index) return null
      if (current > index) return current - 1
      return current
    })
  }, [])

  const changeBoxLabel = useCallback((index: number, nextLabel: string) => {
    const currentLabel = String(correction.boxes?.[index]?.label || '').trim()
    const cleanNextLabel = String(nextLabel || '').trim()

    if (currentLabel !== cleanNextLabel) {
      setHasManualCorrection(true)
    }

    if (cleanNextLabel) {
      setBoxLabel(cleanNextLabel)
    }

    setCorrection((prev) =>
      changeBoxLabelInCorrection(prev, index, nextLabel, labelsRef.current)
    )
  }, [correction.boxes])

  const clearBoxes = useCallback(() => {
    setHasManualCorrection(true)

    setBoxLabel('')
    setActiveBoxIndex(null)

    setCorrection((prev) => ({
      ...prev,
      peoplePresent: [],
      bodyPartsPresent: [],
      objectsPresent: [],
      clothingPresent: [],
      boxes: [],
    }))

    setExpandedCorrectionSections({
      sexPosition: false,
      people: false,
      bodyParts: false,
      objects: false,
      clothing: false,
    })
  }, [])

  const frameBusy = loading || (!!imageSrc && !frameImageLoaded)

  const showImageBoxes = !frameBusy && !trainingRunning

  const shownTrainingDurationMs = useMemo(() => {
    const job = trainingStatus?.training

    if (!job) return 0

    if (job.running && job.startedAt) {
      const started = Date.parse(job.startedAt)
      if (Number.isFinite(started)) {
        return Math.max(0, trainingNowMs - started)
      }
    }

    return trainingDurationMs(job)
  }, [trainingStatus?.training, trainingNowMs])

  const rawTrainingEtaMs = useMemo(() => {
    if (!trainingRunning) return 0

    const job = trainingStatus?.training
    const epoch = Number(job?.epoch ?? 0)
    const epochs = Number(job?.epochs ?? 0)

    if (
      !Number.isFinite(epoch) ||
      !Number.isFinite(epochs) ||
      !Number.isFinite(estimatedEpochMs) ||
      epoch <= 0 ||
      epochs <= 0 ||
      estimatedEpochMs <= 0
    ) {
      return 0
    }

    const completedEpochs = Math.max(1, Math.min(epochs, Math.floor(epoch)))
    const remainingEpochs = Math.max(0, epochs - completedEpochs)

    return Math.max(0, remainingEpochs * estimatedEpochMs)
  }, [
    trainingRunning,
    trainingStatus?.training?.epoch,
    trainingStatus?.training?.epochs,
    estimatedEpochMs,
  ])

  useEffect(() => {
    if (!trainingRunning || rawTrainingEtaMs <= 0) {
      etaSmoothingRef.current = {
        lastAt: 0,
      }

      setSmoothedTrainingEtaMs(0)
      return
    }

    setSmoothedTrainingEtaMs((previous) => {
      const lastAt = etaSmoothingRef.current.lastAt || trainingNowMs
      const elapsed = Math.max(0, trainingNowMs - lastAt)

      // Anzeige zählt zwischen Backend-Updates weiter runter.
      const countedDown = previous > 0
        ? Math.max(0, previous - elapsed)
        : rawTrainingEtaMs

      const diff = rawTrainingEtaMs - countedDown

      // Nach oben sehr vorsichtig glätten, nach unten etwas schneller.
      const factor = diff > 0 ? 0.08 : 0.18

      let next = countedDown + diff * factor

      // Harte Sprünge zusätzlich begrenzen.
      if (diff > 0) {
        next = Math.min(next, countedDown + 10_000)
      } else {
        next = Math.max(next, countedDown - 20_000)
      }

      next = Math.max(0, next)

      etaSmoothingRef.current = {
        lastAt: trainingNowMs,
      }

      return next
    })
  }, [trainingRunning, rawTrainingEtaMs, trainingNowMs])

  const shownTrainingEtaMs = smoothedTrainingEtaMs

  const shownTrainingEpochText = useMemo(() => {
    const epoch = Number(trainingStatus?.training?.epoch ?? 0)
    const epochs = Number(trainingStatus?.training?.epochs ?? 0)

    if (!Number.isFinite(epoch) || !Number.isFinite(epochs)) return ''
    if (epoch <= 0 || epochs <= 0) return ''

    return `Epoche ${epoch}/${epochs}`
  }, [
    trainingStatus?.training?.epoch,
    trainingStatus?.training?.epochs,
  ])

  const activeNotice = useMemo<TrainingNotice | null>(() => {
    if (error) {
      return {
        kind: 'error',
        title: 'Aktion fehlgeschlagen',
        message: error,
      }
    }

    if (message) {
      const lowerMessage = message.toLowerCase()

      const looksPartial =
        lowerMessage.includes('übersprungen') ||
        lowerMessage.includes('fehlgeschlagen') ||
        lowerMessage.includes('abgebrochen')

      return {
        kind: looksPartial ? 'warning' : 'success',
        title: looksPartial ? 'Training teilweise abgeschlossen' : 'Erfolg',
        message,
      }
    }

    if (trainingRunning) {
      const items: TrainingNoticeItem[] = []

      items.push({
        icon: ClockIcon,
        label: 'Bisherige Laufzeit',
        value: formatDuration(shownTrainingDurationMs),
      })

      if (shownTrainingEtaMs > 0) {
        items.push({
          icon: ForwardIcon,
          label: 'Geschätzte Restzeit',
          value: `ca. ${formatDuration(shownTrainingEtaMs)}`,
        })
      }

      if (shownTrainingEpochText) {
        items.push({
          icon: ArrowPathIcon,
          label: 'Epoche',
          value: shownTrainingEpochText.replace(/^Epoche\s+/i, ''),
        })
      }

      if (estimatedEpochMs > 0) {
        items.push({
          icon: BoltIcon,
          label: 'Ø Zeit pro Epoche',
          value: formatDuration(estimatedEpochMs),
        })
      }

      return {
        kind: 'info',
        title: 'Training läuft',
        progress: shownTrainingProgress,
        message: shownTrainingStep || 'Training läuft…',
        items,
      }
    }

    return null
  }, [
    error,
    message,
    trainingRunning,
    shownTrainingStep,
    shownTrainingDurationMs,
    shownTrainingEtaMs,
    shownTrainingEpochText,
    estimatedEpochMs,
    shownTrainingProgress,
  ])

  useEffect(() => {
    if (!message) return
    if (error) return
    if (trainingRunning) return

    const looksPartial =
      message.toLowerCase().includes('übersprungen') ||
      message.toLowerCase().includes('fehlgeschlagen')

    if (looksPartial) return

    const timer = window.setTimeout(() => {
      setMessage(null)
    }, 2500)

    return () => window.clearTimeout(timer)
  }, [message, error, trainingRunning])

  const trainingActionsPanel = (opts?: { compact?: boolean }) => {
    const compact = Boolean(opts?.compact)
    const progress = clampPercent(
      trainingRunning
        ? shownTrainingProgress
        : Math.min(100, (feedbackCount / Math.max(1, requiredCount)) * 100)
    )

    const feedbackReady = feedbackCount >= requiredCount
    const detector = trainingStatus?.detector
    const detectorReady = Boolean(detector?.dataReady)
    const missingTrain = Math.max(
      0,
      Number(detector?.requiredTrain ?? 20) - Number(detector?.trainCount ?? 0)
    )
    const missingVal = Math.max(
      0,
      Number(detector?.requiredVal ?? 3) - Number(detector?.valCount ?? 0)
    )
    const statusText = trainingRunning
      ? shownTrainingStep || 'Training läuft…'
      : !feedbackReady
        ? `${Math.max(0, requiredCount - feedbackCount)} Feedback fehlen noch`
        : !detectorReady
          ? `YOLO-Boxen fehlen: ${missingTrain} Train, ${missingVal} Val`
          : canStartTraining
            ? 'Bereit zum Trainieren'
            : 'Noch nicht trainingsbereit'
    return (
      <div
        className={[
          'relative z-0 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/70',
          compact ? 'p-2' : 'p-3',
        ].join(' ')}
      >
        <div className="flex items-start justify-between gap-3">
          <div className="min-w-0">
            <div className="flex items-center gap-2">
              <div
                className={[
                  'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ring-1',
                  trainingRunning
                    ? 'bg-indigo-50 text-indigo-700 ring-indigo-200 dark:bg-indigo-500/15 dark:text-indigo-200 dark:ring-indigo-400/30'
                    : feedbackReady
                      ? 'bg-emerald-50 text-emerald-700 ring-emerald-200 dark:bg-emerald-500/15 dark:text-emerald-200 dark:ring-emerald-400/30'
                      : 'bg-gray-50 text-gray-600 ring-gray-200 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10',
                ].join(' ')}
              >
                {trainingRunning ? (
                  <ArrowPathIcon className="h-4 w-4 animate-spin" aria-hidden="true" />
                ) : (
                  <BoltIcon className="h-4 w-4" aria-hidden="true" />
                )}
              </div>

              <div className="min-w-0">
                <div className="text-xs font-semibold text-gray-900 dark:text-white">
                  Training-Aktionen
                </div>

                <div className="mt-0.5 truncate text-[11px] text-gray-500 dark:text-gray-400">
                  {statusText}
                </div>
              </div>
            </div>
          </div>

          <button
            type="button"
            onClick={() => setStatsModalOpen(true)}
            className="shrink-0 rounded-full bg-gray-50 px-2 py-1 text-[11px] font-bold text-gray-700 ring-1 ring-gray-200 transition hover:bg-indigo-50 hover:text-indigo-700 hover:ring-indigo-200 dark:bg-white/5 dark:text-gray-200 dark:ring-white/10 dark:hover:bg-indigo-500/20 dark:hover:text-indigo-100 dark:hover:ring-indigo-300/30"
            title="Training-Datenstatistiken anzeigen"
            aria-label="Training-Datenstatistiken anzeigen"
          >
            {feedbackCount}/{requiredCount}
          </button>
        </div>

        {trainingRunning || feedbackCount < requiredCount ? (
          <div className="mt-3">
            <div className="mb-1 flex items-center justify-between text-[10px] font-medium text-gray-500 dark:text-gray-400">
              <span>{trainingRunning ? 'Trainingsfortschritt' : 'Feedback-Fortschritt'}</span>
              <span>{Math.round(progress)}%</span>
            </div>

            <div className="h-2 overflow-hidden rounded-full bg-gray-100 ring-1 ring-black/5 dark:bg-white/10 dark:ring-white/10">
              <div
                className={[
                  'h-full rounded-full transition-all duration-500',
                  trainingRunning
                    ? 'bg-indigo-500'
                    : feedbackReady
                      ? 'bg-emerald-500'
                      : 'bg-gray-400',
                ].join(' ')}
                style={{ width: `${progress}%` }}
              />
            </div>
          </div>
        ) : null}

        <div className="mt-3 grid grid-cols-2 gap-2">
          <Button
            size="md"
            variant="secondary"
            className="w-full justify-center px-2 text-[11px]"
            disabled={uiLocked || !sample}
            onClick={() => void reloadCurrentImage()}
            title="Lädt das aktuelle Bild erneut und führt die Analyse neu aus."
          >
            <span className="inline-flex items-center gap-1.5">
              <ArrowPathIcon className="h-3.5 w-3.5" aria-hidden="true" />
              Neuladen
            </span>
          </Button>

          <Button
            size="md"
            variant={trainingRunning ? 'primary' : canStartTraining ? 'primary' : 'soft'}
            color={trainingRunning ? 'red' : undefined}
            className="w-full justify-center px-2 text-[11px]"
            disabled={
              trainingRunning
                ? cancellingTraining || deletingTrainingData
                : uiLocked || !canStartTraining
            }
            onClick={() => {
              if (trainingRunning) {
                void cancelTraining()
                return
              }

              void startTraining()
            }}
            title={
              trainingRunning
                ? 'Training abbrechen und temporäre Trainingsausgaben löschen.'
                : canStartTraining
                  ? 'YOLO26-Detector mit allen gespeicherten Box-Labels trainieren.'
                  : !feedbackReady
                    ? `Noch zu wenig Feedback: ${feedbackCount}/${requiredCount}.`
                    : !detectorReady
                      ? `Noch zu wenige YOLO-Box-Labels: Train ${detector?.trainCount ?? 0}/${detector?.requiredTrain ?? 20}, Val ${detector?.valCount ?? 0}/${detector?.requiredVal ?? 3}.`
                      : 'Training ist aktuell nicht verfügbar.'
            }
          >
            <span className="inline-flex items-center gap-1.5">
              {trainingRunning ? (
                <XCircleIcon className="h-3.5 w-3.5" aria-hidden="true" />
              ) : (
                <BoltIcon className="h-3.5 w-3.5" aria-hidden="true" />
              )}

              {trainingRunning
                ? cancellingTraining
                  ? 'Breche ab…'
                  : 'Abbrechen'
                : 'Trainieren'}
            </span>
          </Button>
        </div>

        {trainingRunning ? (
          <div className="mt-3 grid grid-cols-2 gap-2 text-[11px]">
            <div className="rounded-lg bg-indigo-50 px-2 py-1.5 text-indigo-900 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-100 dark:ring-indigo-400/20">
              <div className="text-[9px] font-semibold uppercase tracking-wide opacity-70">
                Laufzeit
              </div>
              <div className="mt-0.5 font-bold">
                {formatDuration(shownTrainingDurationMs)}
              </div>
            </div>

            <div className="rounded-lg bg-indigo-50 px-2 py-1.5 text-indigo-900 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:text-indigo-100 dark:ring-indigo-400/20">
              <div className="text-[9px] font-semibold uppercase tracking-wide opacity-70">
                Restzeit
              </div>
              <div className="mt-0.5 font-bold">
                {shownTrainingEtaMs > 0 ? `ca. ${formatDuration(shownTrainingEtaMs)}` : '—'}
              </div>
            </div>
          </div>
        ) : null}

        <div className="mt-3 border-t border-gray-100 pt-3 dark:border-white/10">
          <Button
            size="md"
            variant="primary"
            color="red"
            className="w-full justify-center px-2 text-[11px]"
            disabled={uiLocked}
            onClick={() => void deleteAllTrainingData()}
            title="Löscht Feedback, Frames, Samples und Detector-Daten."
          >
            <span className="inline-flex items-center gap-1.5">
              <TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
              {deletingTrainingData ? 'Lösche…' : 'Trainingsdaten löschen'}
            </span>
          </Button>
        </div>
      </div>
    )
  }

  const detectorBoxesPanel = (opts?: {
    compact?: boolean
    stretch?: boolean
    maxHeightClassName?: string
  }) => {
    const compact = Boolean(opts?.compact)
    const stretch = Boolean(opts?.stretch)
    const hasBoxes = correctionBoxes.length > 0

    return (
      <div
        className={[
          'relative z-0 flex min-h-0 flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm',
          'dark:border-white/10 dark:bg-gray-900/70',
          stretch ? 'h-full max-h-full flex-1' : 'max-h-full',
        ].join(' ')}
      >
        <div
          className={[
            'shrink-0 border-b border-gray-100 bg-gray-50/80 dark:border-white/10 dark:bg-white/[0.03]',
            compact ? 'px-2.5 py-2' : 'px-3 py-2.5',
          ].join(' ')}
        >
          <div className="flex items-start justify-between gap-3">
            <div className="min-w-0">
              <div className="flex items-center gap-2">
                <div className="truncate text-xs font-bold text-gray-900 dark:text-white">
                  Detector-Boxen
                </div>

                <span
                  className={[
                    'rounded-full px-2 py-0.5 text-[10px] font-black ring-1',
                    hasBoxes
                      ? 'bg-blue-50 text-blue-700 ring-blue-200 dark:bg-blue-500/15 dark:text-blue-100 dark:ring-blue-400/30'
                      : 'bg-gray-100 text-gray-500 ring-gray-200 dark:bg-white/10 dark:text-gray-300 dark:ring-white/10',
                  ].join(' ')}
                >
                  {correctionBoxes.length}
                </span>
              </div>

              {!compact ? (
                <div className="mt-0.5 truncate text-[10px] text-gray-500 dark:text-gray-400">
                  Prüfen, Label ändern oder einzeln löschen.
                </div>
              ) : null}
            </div>

            {hasBoxes ? (
              <button
                type="button"
                disabled={uiLocked}
                onClick={clearBoxes}
                className={[
                  'shrink-0 rounded-lg px-2 py-1 text-[10px] font-bold transition ring-1',
                  'bg-white text-red-600 ring-red-100 hover:bg-red-50 hover:text-red-700 hover:ring-red-200',
                  'disabled:cursor-not-allowed disabled:opacity-50',
                  'dark:bg-white/5 dark:text-red-300 dark:ring-red-400/20 dark:hover:bg-red-500/10',
                ].join(' ')}
                title="Alle Boxen entfernen"
              >
                Alle löschen
              </button>
            ) : null}
          </div>
        </div>

        <div
          ref={detectorBoxesScrollRef}
          className={[
            'min-h-0 flex-1 overflow-y-auto overscroll-contain scroll-smooth',
            compact ? 'space-y-1 p-1.5' : 'space-y-1.5 p-2',
            !stretch && (opts?.maxHeightClassName || 'lg:max-h-[32dvh]'),
          ].filter(Boolean).join(' ')}
        >
          {!hasBoxes ? (
            <div className="flex min-h-32 items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50 px-3 py-6 text-center dark:border-white/10 dark:bg-white/[0.03]">
              <div>
                <div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
                  Keine Boxen vorhanden
                </div>

                <div className="mt-1 max-w-[220px] text-[11px] leading-snug text-gray-500 dark:text-gray-400">
                  Wähle rechts ein Label aus und zeichne im Bild eine neue Box.
                </div>
              </div>
            </div>
          ) : (
            correctionBoxes.map((box, index) => {
              const item = getSegmentLabelItem(box.label)
              const Icon = item.icon
              const isActive = activeBoxIndex === index
              const isCorrected = typeof box.score !== 'number'
              const scoreText = typeof box.score === 'number' ? percent(box.score) : 'korrigiert'
              const tone = detectorBoxAppearance(box.label)

              return (
                <div
                  ref={(el) => {
                    detectorBoxItemRefs.current[index] = el
                  }}
                  key={`box-${index}`}
                  onClick={() => setActiveBoxIndex(index)}
                  className={[
                    'group relative cursor-pointer overflow-hidden rounded-2xl border transition-all duration-200',
                    'bg-white shadow-sm',
                    'dark:border-white/10 dark:bg-gray-950/55',
                    isActive
                      ? [
                          'border-gray-200 bg-white',
                          'dark:border-white/10',
                          tone.activeSurface,
                        ].join(' ')
                      : [
                          'border-gray-200 hover:bg-gray-50/80 hover:shadow-md',
                          'dark:border-white/10 dark:hover:bg-white/[0.04]',
                          tone.idleHover,
                        ].join(' '),
                  ].join(' ')}
                >
                  <div
                    className={[
                      'absolute inset-y-2 left-0 w-1 rounded-r-full transition-all duration-200',
                      isActive
                        ? tone.line
                        : ['bg-transparent', tone.lineHover].join(' '),
                    ].join(' ')}
                    aria-hidden="true"
                  />

                  <div className={compact ? 'p-2 pl-3' : 'p-2.5 pl-3.5'}>
                    <div className="flex items-center gap-2.5">
                      <button
                        type="button"
                        disabled={uiLocked}
                        onClick={(e) => {
                          e.stopPropagation()
                          setActiveBoxIndex(index)
                        }}
                        className={[
                          'flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ring-1 transition',
                          isActive
                            ? [
                                'bg-blue-100 text-blue-700 ring-blue-200',
                                tone.iconActive,
                              ].join(' ')
                            : [
                                'bg-gray-50 text-gray-500 ring-gray-200',
                                'group-hover:bg-gray-100 group-hover:text-gray-700',
                                'dark:bg-white/5 dark:ring-white/10',
                                tone.iconIdle,
                              ].join(' '),
                        ].join(' ')}
                        title="Box im Bild markieren"
                        aria-label={`Box ${index + 1} im Bild markieren`}
                      >
                        <Icon className="h-4.5 w-4.5" aria-hidden="true" />
                      </button>

                      <div className="min-w-0 flex-1">
                        <div className="flex min-w-0 items-center justify-between gap-2">
                          <div className="min-w-0">
                            <div className="flex min-w-0 items-center gap-2">
                              <div className="truncate text-[13px] font-bold leading-tight text-gray-950 dark:text-white">
                                {item.text}
                              </div>

                              <span className="shrink-0 text-[10px] font-black text-gray-400 dark:text-gray-500">
                                #{index + 1}
                              </span>
                            </div>

                            <div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-[10px] font-medium leading-tight text-gray-500 dark:text-gray-400">
                              <span
                                className={[
                                  'h-1.5 w-1.5 shrink-0 rounded-full',
                                  isCorrected
                                    ? 'bg-amber-400'
                                    : scoreLevel(box.score) === 'high'
                                      ? 'bg-emerald-400'
                                      : scoreLevel(box.score) === 'mid'
                                        ? 'bg-yellow-400'
                                        : scoreLevel(box.score) === 'low'
                                          ? 'bg-red-400'
                                          : 'bg-gray-300 dark:bg-gray-500',
                                ].join(' ')}
                                aria-hidden="true"
                              />

                              <span className="truncate">
                                {isCorrected ? 'korrigiert' : `Confidence ${scoreText}`}
                              </span>

                              {isActive ? (
                                <>
                                  <span className="text-gray-300 dark:text-gray-600">·</span>
                                  <span
                                    className={[
                                      'font-semibold text-blue-700',
                                      tone.selectedText,
                                    ].join(' ')}
                                  >
                                    ausgewählt
                                  </span>
                                </>
                              ) : null}
                            </div>
                          </div>

                          <button
                            type="button"
                            disabled={uiLocked}
                            className={[
                              'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition',
                              'text-gray-400 hover:bg-red-50 hover:text-red-600',
                              'focus:outline-none focus:ring-2 focus:ring-red-500/30',
                              'disabled:cursor-not-allowed disabled:opacity-50',
                              'dark:text-gray-500 dark:hover:bg-red-500/10 dark:hover:text-red-300',
                            ].join(' ')}
                            title={`${item.text} löschen`}
                            aria-label={`${item.text} löschen`}
                            onClick={(e) => {
                              e.stopPropagation()
                              removeBox(index)
                            }}
                          >
                            <TrashIcon className="h-3.5 w-3.5" aria-hidden="true" />
                          </button>
                        </div>

                        {!isActive ? null : (
                          <div className={compact ? 'mt-2' : 'mt-3'}>
                            <DetectorBoxLabelSelect
                              values={boxLabels}
                              value={box.label}
                              compact={compact}
                              disabled={uiLocked || boxLabels.length === 0}
                              onChange={(value) => changeBoxLabel(index, value)}
                            />
                          </div>
                        )}
                      </div>
                    </div>
                  </div>
                </div>
              )
            })
          )}
        </div>

        {hasBoxes ? (
          <div className="shrink-0 border-t border-gray-100 bg-gray-50/80 p-2 dark:border-white/10 dark:bg-white/[0.03]">
            <Button
              size="md"
              variant="secondary"
              className={compact ? 'w-full text-xs' : 'w-full shrink-0'}
              disabled={uiLocked}
              onClick={clearBoxes}
            >
              Alle Boxen entfernen
            </Button>
          </div>
        ) : null}
      </div>
    )
  }

  const imageTouchClass =
    boxLabel || drawingBox || boxInteraction
      ? 'touch-none'
      : 'touch-pan-y'

  return (
    <div className="min-h-full lg:h-full lg:min-h-0 lg:overflow-hidden">
      <div className="mb-2 flex items-center justify-between gap-2 rounded-xl border border-gray-200 bg-white px-3 py-2 shadow-sm dark:border-white/10 dark:bg-gray-900/80 lg:hidden">
        <div className="min-w-0">
          <div className="truncate text-sm font-semibold text-gray-900 dark:text-white">
            Training
          </div>
        </div>

        <button
          type="button"
          onClick={() => setStatsModalOpen(true)}
          className="ml-auto shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-bold leading-none text-gray-700 ring-1 ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10"
          title="Training-Datenstatistiken anzeigen"
          aria-label="Training-Datenstatistiken anzeigen"
        >
          {feedbackCount}/{requiredCount}
        </button>
      </div>

      <div className="grid grid-cols-1 items-start gap-2 lg:h-full lg:min-h-0 lg:overflow-hidden lg:grid-cols-[300px_minmax(0,1fr)_300px] xl:grid-cols-[320px_minmax(0,1fr)_320px]">
        {/* Sidebar links */}
        <aside className="hidden h-full max-h-full min-h-0 flex-col overflow-hidden rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900/60 lg:flex">
          <div className="flex items-center justify-between gap-2">
            <div className="min-w-0 text-sm font-semibold text-gray-900 dark:text-white">
              Training
            </div>

            <button
              type="button"
              onClick={() => setStatsModalOpen(true)}
              className={[
                'shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200 transition',
                'hover:bg-indigo-50 hover:text-indigo-700 hover:ring-indigo-200',
                'focus:outline-none focus:ring-2 focus:ring-indigo-500/40',
                'dark:bg-white/10 dark:text-gray-200 dark:ring-white/10 dark:hover:bg-indigo-500/20 dark:hover:text-indigo-100 dark:hover:ring-indigo-300/30',
              ].join(' ')}
              title="Training-Datenstatistiken anzeigen"
              aria-label="Training-Datenstatistiken anzeigen"
            >
              {feedbackCount}/{requiredCount}
            </button>
          </div>

          <div className="mt-2 text-xs text-gray-600 dark:text-gray-300">
            Bestätige oder korrigiere die Analyse. Jede Antwort wird als Trainingsdatenpunkt gespeichert.
          </div>

          <div className="mt-3 flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
            <div className="min-h-0 flex-1 rounded-xl overflow-hidden">
              {detectorBoxesPanel({
                stretch: true,
                maxHeightClassName: 'lg:max-h-none',
              })}
            </div>

            <div className="shrink-0">
              {trainingActionsPanel()}
            </div>
          </div>
        </aside>

        {/* Mitte */}
        <section className="min-w-0 rounded-xl border border-gray-200 bg-white p-2 shadow-sm dark:border-white/10 dark:bg-gray-900/60 sm:p-3 lg:self-start">
          <div className="relative flex min-h-[180px] flex-1 items-center justify-center overflow-visible rounded-lg bg-black p-2 sm:p-3 lg:min-h-[300px]">
            {imageSrc ? (
              <div className="relative flex h-full w-full items-center justify-center">
                <div
                  ref={imageBoxRef}
                  className={[
                    'relative inline-block max-h-[52dvh] max-w-full select-none overscroll-contain transition sm:max-h-[60dvh] lg:max-h-[72dvh]',
                    imageTouchClass,
                    '[-webkit-touch-callout:none] [-webkit-user-select:none] [user-select:none]',
                    trainingRunning || loading ? 'pointer-events-none' : '',
                  ].join(' ')}
                  onContextMenu={(e) => e.preventDefault()}
                  onPointerDown={startDrawBox}
                  onPointerMove={moveDrawBox}
                  onPointerUp={finishDrawBox}
                  onPointerCancel={finishDrawBox}
                >
                  <img
                    src={imageSrc}
                    alt="Training Frame"
                    draggable={false}
                    onLoad={() => setFrameImageLoaded(true)}
                    onError={() => setFrameImageLoaded(true)}
                    onContextMenu={(e) => e.preventDefault()}
                    onDragStart={(e) => e.preventDefault()}
                    className={[
                      'block rounded-lg max-h-[52dvh] max-w-full object-contain sm:max-h-[60dvh] lg:max-h-[72dvh]',
                      'select-none',
                      imageTouchClass,
                      '[-webkit-user-drag:none] [-webkit-touch-callout:none]',
                    ].join(' ')}
                  />

                  {showImageBoxes ? (
                    <div className="absolute inset-0">
                      {visibleBoxes.map(({ box, index, isDraft }) => {
                        const left = clampPercent(box.x * 100)
                        const top = clampPercent(box.y * 100)
                        const width = clampPercent(box.w * 100)
                        const height = clampPercent(box.h * 100)

                        const item = getSegmentLabelItem(box.label)
                        const Icon = item.icon
                        const isSmallBox = width < 18 || height < 12
                        const isActiveBox = !isDraft && activeBoxIndex === index

                        return (
                          <div
                            key={`${box.label}-${index}-${isDraft ? 'draft' : 'box'}`}
                            className={[
                              'pointer-events-none absolute rounded border-2',
                              isActiveBox
                                ? 'border-blue-500 shadow-none'
                                : [
                                    'shadow-none',
                                    scoreBorderClass(box.score, { draft: isDraft }),
                                  ].join(' '),
                            ].join(' ')}
                            style={{
                              left: `${left}%`,
                              top: `${top}%`,
                              width: `${width}%`,
                              height: `${height}%`,
                              backgroundColor: 'transparent',
                              zIndex: isActiveBox ? 30 : isDraft ? 25 : 10,
                            }}
                            title={box.label}
                          >
                            <div
                              data-box-control="true"
                              className={[
                                'pointer-events-auto absolute left-0 top-0 z-[35] flex -translate-y-[calc(100%+4px)] touch-none select-none items-center',
                                '[-webkit-user-select:none] [-webkit-touch-callout:none]',
                                isSmallBox
                                  ? 'max-w-none'
                                  : 'max-w-[min(210px,calc(100vw-24px))]',
                              ].join(' ')}
                              title={isDraft ? box.label : `${item.text} verschieben`}
                            >
                              <button
                                type="button"
                                className={[
                                  'group/label flex h-5 min-w-0 touch-none select-none items-center rounded-full text-left',
                                  isSmallBox ? 'gap-0.5 px-1' : 'gap-1 pl-1.5 pr-0.5',
                                  'text-[10px] font-bold leading-none shadow-md ring-1 transition',
                                  '[-webkit-user-select:none] [-webkit-touch-callout:none]',
                                  isActiveBox
                                    ? [
                                        'bg-white/95 text-blue-700 ring-blue-500/30 hover:bg-blue-50',
                                        'dark:bg-blue-600 dark:text-white dark:ring-white/25 dark:hover:bg-blue-700',
                                      ].join(' ')
                                    : isDraft
                                      ? [
                                          'bg-amber-100/95 text-amber-950 ring-amber-500/30',
                                          'dark:bg-amber-400 dark:text-black dark:ring-black/15',
                                        ].join(' ')
                                      : [
                                        'bg-white/95 text-gray-950 ring-black/10 hover:bg-gray-50',
                                        'dark:bg-white/95 dark:text-gray-950 dark:ring-black/10 dark:hover:bg-gray-50',
                                      ].join(' '),
                                  isDraft ? 'cursor-default' : 'cursor-move',
                                ].join(' ')}
                                disabled={Boolean(isDraft) || uiLocked}
                                onPointerDown={(e) => {
                                  if (isDraft || uiLocked) return

                                  e.preventDefault()
                                  e.stopPropagation()
                                  window.getSelection()?.removeAllRanges()

                                  const target = e.target as HTMLElement | null
                                  if (target?.closest('[data-box-trash="true"]')) return

                                  const requiresActivationFirst = !isActiveBox

                                  setActiveBoxIndex(index)

                                  if (requiresActivationFirst) return

                                  const pos = getPointerPosInImage(e.clientX, e.clientY, { clamp: false })
                                  const clampedPos = getPointerPosInImage(e.clientX, e.clientY)

                                  if (!pos || !clampedPos) return

                                  try {
                                    imageBoxRef.current?.setPointerCapture(e.pointerId)
                                  } catch {
                                    // Pointer wurde vom Browser bereits abgebrochen.
                                  }

                                  setTouchMagnifier({
                                    visible: true,
                                    clientX: e.clientX,
                                    clientY: e.clientY,
                                    imageX: clampedPos.x,
                                    imageY: clampedPos.y,
                                  })

                                  setBoxInteraction({
                                    type: 'move',
                                    index,
                                    startX: pos.x,
                                    startY: pos.y,
                                    original: box,
                                  })
                                }}
                              >
                                <span
                                  className={[
                                    'shrink-0 rounded-full px-1 py-0.5 text-[9px] font-black leading-none',
                                    isActiveBox
                                      ? 'bg-blue-100 text-blue-700 dark:bg-white/20 dark:text-white'
                                      : isDraft
                                        ? 'bg-amber-200 text-amber-950 dark:bg-black/15 dark:text-black'
                                        : 'bg-gray-100 text-gray-700 dark:bg-gray-100 dark:text-gray-700',
                                  ].join(' ')}
                                >
                                  {isDraft ? 'neu' : `#${index + 1}`}
                                </span>

                                <Icon
                                  className="h-3 w-3 shrink-0 text-current"
                                  aria-hidden="true"
                                />

                                {!isSmallBox ? (
                                  <span className="min-w-0 max-w-[104px] truncate text-current sm:max-w-[132px]">
                                    {item.text}
                                  </span>
                                ) : null}

                                {!isDraft ? (
                                  <span
                                    data-box-trash="true"
                                    role="button"
                                    tabIndex={0}
                                    className={[
                                      'ml-0.5 flex h-4.5 w-4.5 shrink-0 cursor-pointer touch-none items-center justify-center rounded-full',
                                      '!bg-red-600 !text-white ring-1 ring-red-800/40 transition',
                                      'hover:!bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500/60',
                                      'dark:!bg-red-500 dark:!text-white dark:ring-white/20 dark:hover:!bg-red-600',
                                    ].join(' ')}
                                    title={`${box.label} löschen`}
                                    aria-label={`${box.label} löschen`}
                                    onPointerDown={(e) => {
                                      e.preventDefault()
                                      e.stopPropagation()
                                    }}
                                    onClick={(e) => {
                                      e.preventDefault()
                                      e.stopPropagation()
                                      removeBox(index)
                                    }}
                                    onKeyDown={(e) => {
                                      if (e.key !== 'Enter' && e.key !== ' ') return
                                      e.preventDefault()
                                      e.stopPropagation()
                                      removeBox(index)
                                    }}
                                  >
                                    <TrashIcon
                                      className="h-3 w-3 shrink-0  !text-white"
                                      aria-hidden="true"
                                    />
                                  </span>
                                ) : null}
                              </button>
                            </div>

                            {!isDraft ? (
                              <>
                                {(['nw', 'ne', 'sw', 'se'] as const).map((handle) => (
                                  <button
                                    key={handle}
                                    data-box-control="true"
                                    type="button"
                                    disabled={uiLocked}
                                    className={[
                                      isActiveBox
                                        ? [
                                            'pointer-events-auto absolute z-[34] flex h-7 w-7 touch-none items-center justify-center',
                                            'appearance-none rounded-none border-0 p-0 opacity-100 shadow-none ring-0',
                                            '!bg-transparent hover:!bg-transparent active:!bg-transparent focus:!bg-transparent',
                                            'focus:outline-none disabled:opacity-50 sm:h-5 sm:w-5',
                                          ].join(' ')
                                        : [
                                            'pointer-events-none absolute z-20 hidden h-7 w-7 touch-none items-center justify-center',
                                            'appearance-none rounded-none border-0 p-0 opacity-100 shadow-none ring-0',
                                            '!bg-transparent hover:!bg-transparent active:!bg-transparent focus:!bg-transparent',
                                            'focus:outline-none disabled:opacity-50 sm:h-5 sm:w-5',
                                          ].join(' '),
                                      handle === 'nw' ? '-left-3.5 -top-3.5 cursor-nwse-resize sm:-left-2.5 sm:-top-2.5' : '',
                                      handle === 'ne' ? '-right-3.5 -top-3.5 cursor-nesw-resize sm:-right-2.5 sm:-top-2.5' : '',
                                      handle === 'sw' ? '-bottom-3.5 -left-3.5 cursor-nesw-resize sm:-bottom-2.5 sm:-left-2.5' : '',
                                      handle === 'se' ? '-bottom-3.5 -right-3.5 cursor-nwse-resize sm:-bottom-2.5 sm:-right-2.5' : '',
                                    ].join(' ')}
                                    title="Boxgröße ändern"
                                    onPointerDown={(e) => {
                                      if (uiLocked) return

                                      e.preventDefault()
                                      e.stopPropagation()
                                      window.getSelection()?.removeAllRanges()

                                      const requiresActivationFirst = !isActiveBox

                                      setActiveBoxIndex(index)

                                      // Erster Klick/Touch aktiviert nur die Box.
                                      // Erst danach darf Resize starten.
                                      if (requiresActivationFirst) return

                                      const pos = getPointerPosInImage(e.clientX, e.clientY)
                                      if (!pos) return

                                      try {
                                        imageBoxRef.current?.setPointerCapture(e.pointerId)
                                      } catch {
                                        // Pointer wurde vom Browser bereits abgebrochen.
                                      }

                                      setTouchMagnifier({
                                        visible: true,
                                        clientX: e.clientX,
                                        clientY: e.clientY,
                                        imageX: pos.x,
                                        imageY: pos.y,
                                      })

                                      setBoxInteraction({
                                        type: 'resize',
                                        index,
                                        handle,
                                        startX: pos.x,
                                        startY: pos.y,
                                        original: box,
                                      })
                                    }}
                                  >
                                    <span
                                      className={[
                                        'block h-2 w-2 rounded-full bg-white shadow-none ring-2 sm:h-2 sm:w-2',
                                        isActiveBox ? 'ring-blue-500' : scoreRingClass(box.score),
                                      ].join(' ')}
                                    />
                                  </button>
                                ))}
                              </>
                            ) : null}
                          </div>
                        )
                      })}
                    </div>
                  ) : null}

                  {touchMagnifier?.visible && imageSrc && showImageBoxes ? (() => {
                    const el = imageBoxRef.current
                    const rect = el?.getBoundingClientRect()

                    if (!rect || rect.width <= 0 || rect.height <= 0) return null

                    const size = 156
                    const padding = 20

                    const activeBox =
                      drawingBox ||
                      (boxInteraction
                        ? correction.boxes?.[boxInteraction.index] ?? null
                        : null)

                    const hasUsableBox =
                      activeBox &&
                      Number.isFinite(activeBox.w) &&
                      Number.isFinite(activeBox.h) &&
                      activeBox.w > 0.003 &&
                      activeBox.h > 0.003

                    const boxCenterX = hasUsableBox
                      ? clamp01(activeBox.x + activeBox.w / 2)
                      : touchMagnifier.imageX

                    const boxCenterY = hasUsableBox
                      ? clamp01(activeBox.y + activeBox.h / 2)
                      : touchMagnifier.imageY

                    const boxPixelW = hasUsableBox ? activeBox.w * rect.width : 0
                    const boxPixelH = hasUsableBox ? activeBox.h * rect.height : 0

                    const fitZoom = hasUsableBox
                      ? Math.min(
                          (size - padding * 2) / Math.max(1, boxPixelW),
                          (size - padding * 2) / Math.max(1, boxPixelH)
                        )
                      : 2

                    const zoom = hasUsableBox
                      ? Math.max(0.55, Math.min(2.25, fitZoom))
                      : 2

                    const viewportW = typeof window !== 'undefined' ? window.innerWidth : 390
                    const viewportH = typeof window !== 'undefined' ? window.innerHeight : 800

                    let left = touchMagnifier.clientX - size / 2
                    let top = touchMagnifier.clientY - size - 28

                    if (top < 8) {
                      top = touchMagnifier.clientY + 28
                    }

                    left = Math.max(8, Math.min(viewportW - size - 8, left))
                    top = Math.max(8, Math.min(viewportH - size - 8, top))

                    const imageWidth = rect.width * zoom
                    const imageHeight = rect.height * zoom

                    const imageLeft = size / 2 - boxCenterX * imageWidth
                    const imageTop = size / 2 - boxCenterY * imageHeight

                    const pointerX = imageLeft + touchMagnifier.imageX * imageWidth
                    const pointerY = imageTop + touchMagnifier.imageY * imageHeight

                    const boxLeft = hasUsableBox ? imageLeft + activeBox.x * imageWidth : 0
                    const boxTop = hasUsableBox ? imageTop + activeBox.y * imageHeight : 0
                    const boxWidth = hasUsableBox ? activeBox.w * imageWidth : 0
                    const boxHeight = hasUsableBox ? activeBox.h * imageHeight : 0

                    return (
                      <div
                        className="pointer-events-none fixed z-[9999] overflow-hidden rounded-xl border-2 border-white bg-black shadow-2xl ring-2 ring-black/70"
                        style={{
                          left,
                          top,
                          width: size,
                          height: size,
                        }}
                      >
                        <img
                          src={imageSrc}
                          alt=""
                          draggable={false}
                          className="absolute max-h-none max-w-none select-none"
                          style={{
                            left: imageLeft,
                            top: imageTop,
                            width: imageWidth,
                            height: imageHeight,
                          }}
                        />

                        {hasUsableBox ? (
                          <div
                            className={[
                              'absolute rounded border-2 bg-black/0 shadow-[0_0_0_1px_rgba(0,0,0,0.85)]',
                              scoreBorderClass(activeBox.score, { draft: Boolean(drawingBox) }),
                            ].join(' ')}
                            style={{
                              left: boxLeft,
                              top: boxTop,
                              width: boxWidth,
                              height: boxHeight,
                            }}
                          />
                        ) : null}

                        <div
                          className="absolute h-5 w-px -translate-x-1/2 -translate-y-1/2 bg-red-500/90"
                          style={{
                            left: pointerX,
                            top: pointerY,
                          }}
                        />
                        <div
                          className="absolute h-px w-5 -translate-x-1/2 -translate-y-1/2 bg-red-500/90"
                          style={{
                            left: pointerX,
                            top: pointerY,
                          }}
                        />

                        <div
                          className="absolute h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white bg-red-500 shadow"
                          style={{
                            left: pointerX,
                            top: pointerY,
                          }}
                        />
                      </div>
                    )
                  })() : null}
                </div>

                {trainingRunning ? (
                  <TrainingOverlay
                    step={shownTrainingStep}
                    progress={shownTrainingProgress}
                  />
                ) : frameBusy ? (
                  <LoadingImageOverlay
                    text={analysisStep || 'Bild wird geladen…'}
                    progress={loading ? analysisProgress : 100}
                  />
                ) : null}
              </div>
            ) : trainingRunning ? (
              <TrainingOverlay
                step={shownTrainingStep || 'Aktuelles Bild wird geladen…'}
                progress={shownTrainingProgress}
              />
            ) : loading ? (
              <LoadingImageOverlay
                text={analysisStep}
                progress={analysisProgress}
              />
            ) : (
              <div className="text-sm text-white/80">Kein Bild geladen</div>
            )}
          </div>

          <div className="sticky bottom-0 z-40 mt-3 grid grid-cols-2 gap-2 sm:static sm:mt-4">
            <Button
              size="md"
              variant={hasManualCorrection ? 'primary' : 'soft'}
              color={hasManualCorrection ? undefined : 'emerald'}
              disabled={
                uiLocked ||
                frameBusy ||
                !sample ||
                (!hasManualCorrection && !sample.prediction.modelAvailable)
              }
              onClick={() => void saveFeedback(!hasManualCorrection)}
              className="w-full justify-center px-2 text-xs sm:text-sm"
              title={
                hasManualCorrection
                  ? 'Die korrigierten Werte werden gespeichert.'
                  : sample?.prediction.modelAvailable
                    ? 'Die Erkennung stimmt. Prediction wird als korrekt gespeichert.'
                    : 'Erst verfügbar, wenn ein Modell trainiert wurde.'
              }
            >
              {saving ? (
                'Speichere…'
              ) : hasManualCorrection ? (
                <>
                  <span className="sm:hidden">Speichern</span>
                  <span className="hidden sm:inline">Speichern & weiter</span>
                </>
              ) : (
                <>
                  <span className="sm:hidden">Passt</span>
                  <span className="hidden sm:inline">Passt so & weiter</span>
                </>
              )}
            </Button>

            <Button
              size="md"
              variant="secondary"
              disabled={uiLocked || frameBusy || !sample}
              onClick={() => void loadNext({ forceNew: true })}
              className="w-full justify-center px-2 text-xs sm:text-sm"
              title="Dieses Bild nicht bewerten und ein anderes laden."
            >
              <span className="sm:hidden">Skip</span>
              <span className="hidden sm:inline">Überspringen</span>
            </Button>
          </div>

          {activeNotice ? (
            <div className="mx-auto mt-3 hidden w-full max-w-2xl sm:block">
              <TrainingNoticeCard
                notice={activeNotice}
                onClose={
                  trainingRunning
                    ? undefined
                    : () => {
                        setError(null)
                        setMessage(null)
                      }
                }
              />
            </div>
          ) : (
            <div className="mt-2 hidden text-center text-xs text-gray-500 dark:text-gray-400 lg:block">
              Prüfe das Bild. Wenn die Erkennung stimmt: „Passt so“. Wenn nicht: korrigieren und speichern.
            </div>
          )}
        </section>
        
        <div className="flex h-[48dvh] min-h-[260px] flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-900/80 lg:hidden">
          <div className="grid grid-cols-3 gap-1 border-b border-gray-200 p-1 dark:border-white/10">
            {[
              { key: 'labels', label: 'Labels' },
              { key: 'boxes', label: 'Boxen', count: correctionBoxes.length },
              { key: 'training', label: 'Training' },
            ].map((item) => {
              const active = mobilePanel === item.key

              return (
                <button
                  key={item.key}
                  type="button"
                  onClick={() => setMobilePanel(item.key as 'labels' | 'boxes' | 'training')}
                  className={[
                    'rounded-md px-2 py-2 text-xs font-semibold transition',
                    active
                      ? [
                          'bg-indigo-100 text-indigo-900 shadow-sm ring-1 ring-indigo-200',
                          'dark:bg-indigo-600 dark:text-white dark:ring-indigo-500',
                        ].join(' ')
                      : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/10',
                  ].join(' ')}
                >
                  <span className="inline-flex items-center justify-center gap-1.5">
                    <span>{item.label}</span>

                    {typeof item.count === 'number' ? (
                      <span
                        className={[
                          'min-w-5 rounded-full px-1.5 py-0.5 text-[10px] font-black leading-none ring-1',
                          active
                            ? 'bg-indigo-600 text-white ring-indigo-600 dark:bg-white dark:text-indigo-700 dark:ring-white'
                            : 'bg-gray-100 text-gray-700 ring-gray-200 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10',
                        ].join(' ')}
                        aria-label={`${item.count} Boxen`}
                      >
                        {item.count}
                      </span>
                    ) : null}
                  </span>
                </button>
              )
            })}
          </div>

          <div
            ref={mobileLabelsScrollRef}
            className="min-h-0 h-full flex-1 overflow-y-auto overscroll-contain p-3 scroll-smooth"
          >
            {mobilePanel === 'labels' ? (
              <div
                className={[
                  'space-y-3',
                  uiLocked ? 'pointer-events-none opacity-60' : '',
                ].join(' ')}
              >
                <div className="flex items-start justify-between gap-3">
                  <div className="min-w-0">
                    <div className="text-sm font-semibold text-gray-900 dark:text-white">
                      Korrektur
                    </div>

                    <div className="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
                      Label wählen, dann Box im Bild zeichnen.
                    </div>
                  </div>

                  <span
                    className={[
                      'shrink-0 rounded-full px-2.5 py-1 text-[11px] font-bold ring-1',
                      confidencePillClass(analysisConfidence),
                    ].join(' ')}
                    title={`Analyse-Confidence aktuelles Bild: ${confidencePercent(analysisConfidence)}`}
                  >
                    {confidencePercent(analysisConfidence)}
                  </span>
                </div>

                <div
                  ref={(el) => {
                    mobileSectionRefs.current.sexPosition = el
                  }}
                >
                  <CollapsibleSingleLabelSection
                    title="Sexposition"
                    values={labels.sexPositions}
                    value={correction.sexPosition}
                    score={sample?.prediction.sexPositionScore}
                    predictionValue={sample?.prediction.sexPosition}
                    expanded={expandedCorrectionSections.sexPosition}
                    onExpandedChange={(expanded) =>
                      toggleMobileCorrectionSection('sexPosition', expanded)
                    }
                    onChange={(value) =>
                      setCorrection((p) => {
                        if (p.sexPosition !== value) {
                          setHasManualCorrection(true)
                        }

                        return {
                          ...p,
                          sexPosition: value,
                        }
                      })
                    }
                    disabled={uiLocked}
                    gridClassName="grid grid-cols-3 gap-2"
                  />
                </div>

                <div
                  ref={(el) => {
                    mobileSectionRefs.current.people = el
                  }}
                >
                  <CollapsibleLabelSection
                    title="Personen"
                    values={labels.people}
                    selected={selectedPeopleLabels}
                    scores={peopleScores}
                    expanded={expandedCorrectionSections.people}
                    onExpandedChange={(expanded) =>
                      toggleMobileCorrectionSection('people', expanded)
                    }
                    onToggle={() => {}}
                    drawLabel={drawLabelForSection(labels.people)}
                    onDrawLabelChange={setBoxLabel}
                    disabled={uiLocked}
                    singleDrawMode
                    gridClassName="grid grid-cols-2 gap-2"
                  />
                </div>

                <div
                  ref={(el) => {
                    mobileSectionRefs.current.bodyParts = el
                  }}
                >
                  <CollapsibleLabelSection
                    title="Körperteile"
                    values={labels.bodyParts}
                    selected={correction.bodyPartsPresent}
                    scores={bodyPartScores}
                    expanded={expandedCorrectionSections.bodyParts}
                    gridClassName="grid grid-cols-3 gap-2"
                      onExpandedChange={(expanded) =>
                        toggleMobileCorrectionSection('bodyParts', expanded)
                      }
                    onToggle={(value) =>
                      setCorrection((p) => {
                        setHasManualCorrection(true)

                        return {
                          ...p,
                          bodyPartsPresent: toggleArrayValue(p.bodyPartsPresent, value),
                        }
                      })
                    }
                    drawLabel={drawLabelForSection(labels.bodyParts)}
                    onDrawLabelChange={setBoxLabel}
                    disabled={uiLocked}
                  />
                </div>

                <div
                  ref={(el) => {
                    mobileSectionRefs.current.objects = el
                  }}
                >
                  <CollapsibleLabelSection
                    title="Gegenstände"
                    values={labels.objects}
                    selected={correction.objectsPresent}
                    scores={objectScores}
                    expanded={expandedCorrectionSections.objects}
                    gridClassName="grid grid-cols-3 gap-2"
                    onExpandedChange={(expanded) =>
                      toggleMobileCorrectionSection('objects', expanded)
                    }
                    onToggle={(value) =>
                      setCorrection((p) => {
                        setHasManualCorrection(true)

                        return {
                          ...p,
                          objectsPresent: toggleArrayValue(p.objectsPresent, value),
                        }
                      })
                    }
                    drawLabel={drawLabelForSection(labels.objects)}
                    onDrawLabelChange={setBoxLabel}
                    disabled={uiLocked}
                  />
                </div>

                <div
                  ref={(el) => {
                    mobileSectionRefs.current.clothing = el
                  }}
                >
                  <CollapsibleLabelSection
                    title="Kleidung"
                    values={labels.clothing}
                    selected={correction.clothingPresent}
                    scores={clothingScores}
                    expanded={expandedCorrectionSections.clothing}
                    gridClassName="grid grid-cols-3 gap-2"
                    onExpandedChange={(expanded) =>
                      toggleMobileCorrectionSection('clothing', expanded)
                    }
                    onToggle={(value) =>
                      setCorrection((p) => {
                        setHasManualCorrection(true)

                        return {
                          ...p,
                          clothingPresent: toggleArrayValue(p.clothingPresent, value),
                        }
                      })
                    }
                    drawLabel={drawLabelForSection(labels.clothing)}
                    onDrawLabelChange={setBoxLabel}
                    disabled={uiLocked}
                  />
                </div>
              </div>
            ) : null}

            {mobilePanel === 'boxes' ? (
              <div>
                {detectorBoxesPanel({ compact: true })}
              </div>
            ) : null}

            {mobilePanel === 'training' ? (
              <div className="space-y-2 text-xs">
                <div className="pt-1">
                  {trainingActionsPanel({ compact: true })}
                </div>
              </div>
            ) : null}
          </div>
        </div>

        {/* Rechte Details/Korrektur */}
        <aside
          className={[
            'hidden h-full max-h-full min-h-0 overflow-y-auto overscroll-contain rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900/60 lg:block',
            uiLocked ? 'pointer-events-none opacity-60' : ''
          ].join(' ')}
        >
          <div className="flex items-start justify-between gap-3">
            <div className="min-w-0">
              <div className="text-sm font-semibold text-gray-900 dark:text-white">
                Korrektur
              </div>

              <div className="mt-1 text-xs text-gray-500 dark:text-gray-400">
                Diese Werte werden gespeichert, wenn du „Korrektur speichern & weiter“ klickst.
              </div>
            </div>

            <span
              className={[
                'shrink-0 rounded-full px-2.5 py-1 text-[11px] font-bold ring-1',
                confidencePillClass(analysisConfidence),
              ].join(' ')}
              title={`Analyse-Confidence aktuelles Bild: ${confidencePercent(analysisConfidence)}`}
            >
              {confidencePercent(analysisConfidence)}
            </span>
          </div>

          <div className="mt-3 space-y-3">
            <CollapsibleSingleLabelSection
              title="Sexposition"
              values={labels.sexPositions}
              value={correction.sexPosition}
              score={sample?.prediction.sexPositionScore}
              predictionValue={sample?.prediction.sexPosition}
              expanded={expandedCorrectionSections.sexPosition}
              onExpandedChange={(expanded) =>
                setExpandedCorrectionSections((p) => ({
                  ...p,
                  sexPosition: expanded,
                }))
              }
              onChange={(value) =>
                setCorrection((p) => {
                  if (p.sexPosition !== value) {
                    setHasManualCorrection(true)
                  }

                  return {
                    ...p,
                    sexPosition: value,
                  }
                })
              }
              disabled={uiLocked}
              gridClassName="grid grid-cols-3 gap-2"
            />
            
            <CollapsibleLabelSection
              title="Personen"
              values={labels.people}
              selected={selectedPeopleLabels}
              scores={peopleScores}
              expanded={expandedCorrectionSections.people}
              onExpandedChange={(expanded) =>
                setExpandedCorrectionSections((p) => ({
                  ...p,
                  people: expanded,
                }))
              }
              onToggle={() => {}}
              drawLabel={drawLabelForSection(labels.people)}
              onDrawLabelChange={setBoxLabel}
              disabled={uiLocked}
              singleDrawMode
              gridClassName="grid grid-cols-2 gap-2"
            />

            <CollapsibleLabelSection
              title="Körperteile"
              values={labels.bodyParts}
              selected={correction.bodyPartsPresent}
              scores={bodyPartScores}
              expanded={expandedCorrectionSections.bodyParts}
              gridClassName="grid grid-cols-3 gap-2"
              onExpandedChange={(expanded) =>
                setExpandedCorrectionSections((p) => ({
                  ...p,
                  bodyParts: expanded,
                }))
              }
             onToggle={(value) =>
                setCorrection((p) => {
                  setHasManualCorrection(true)

                  return {
                    ...p,
                    bodyPartsPresent: toggleArrayValue(p.bodyPartsPresent, value),
                  }
                })
              }
              drawLabel={drawLabelForSection(labels.bodyParts)}
              onDrawLabelChange={setBoxLabel}
              disabled={uiLocked}
            />

            <CollapsibleLabelSection
              title="Gegenstände"
              values={labels.objects}
              selected={correction.objectsPresent}
              scores={objectScores}
              expanded={expandedCorrectionSections.objects}
              gridClassName="grid grid-cols-3 gap-2"
              onExpandedChange={(expanded) =>
                setExpandedCorrectionSections((p) => ({
                  ...p,
                  objects: expanded,
                }))
              }
              onToggle={(value) =>
                setCorrection((p) => {
                  setHasManualCorrection(true)

                  return {
                    ...p,
                    objectsPresent: toggleArrayValue(p.objectsPresent, value),
                  }
                })
              }
              drawLabel={drawLabelForSection(labels.objects)}
              onDrawLabelChange={setBoxLabel}
              disabled={uiLocked}
            />

            <CollapsibleLabelSection
              title="Kleidung"
              values={labels.clothing}
              selected={correction.clothingPresent}
              scores={clothingScores}
              expanded={expandedCorrectionSections.clothing}
              gridClassName="grid grid-cols-3 gap-2"
              onExpandedChange={(expanded) =>
                setExpandedCorrectionSections((p) => ({
                  ...p,
                  clothing: expanded,
                }))
              }
              onToggle={(value) =>
                setCorrection((p) => {
                  setHasManualCorrection(true)

                  return {
                    ...p,
                    clothingPresent: toggleArrayValue(p.clothingPresent, value),
                  }
                })
              }
              drawLabel={drawLabelForSection(labels.clothing)}
              onDrawLabelChange={setBoxLabel}
              disabled={uiLocked}
            />
          </div>
        </aside>
      </div>

      <TrainingStatsModal
        open={statsModalOpen}
        onClose={() => setStatsModalOpen(false)}
        stats={trainingStats}
        loading={trainingStatsLoading}
        error={trainingStatsError}
        feedbackCount={feedbackCount}
        requiredCount={requiredCount}
      />
    </div>
  )
}