import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { Link } from 'react-router-dom'
import {
  Eye,
  Pencil,
  Trash2,
  Globe,
  Users,
  Lock,
  CheckCircle2,
  Archive,
  CircleDot,
  FilePenLine,
} from 'lucide-react'
import api from '../utils/api'
import { useAuth } from '../context/AuthContext'
import { activeClubMemberships } from '../utils/activeClub'
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
import MultiSelectCombo from '../components/MultiSelectCombo'
import ExerciseFocusRulePicker from '../components/ExerciseFocusRulePicker'
import CatalogRulePicker from '../components/CatalogRulePicker'
import ExerciseProgressionGraphPanel from '../components/ExerciseProgressionGraphPanel'
import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock'
import PageSectionNav from '../components/PageSectionNav'
import {
  INITIAL_EXERCISE_LIST_FILTERS,
  mergeExerciseListPrefsFromApi,
  compactExerciseListPrefsPayload,
  splitMnCatalogRules,
  splitScalarCatalogRules,
} from '../constants/exerciseListFilters'
import { coerceApiNameList } from '../utils/sanitizeHtml'
import { canUserRequestExerciseDelete } from '../utils/exercisePermissions'

const PAGE_SIZE = 100
const BULK_MAX_IDS = 500
const EXERCISES_PAGE_TABS = [
  { id: 'list', label: 'Liste' },
  { id: 'progression', label: 'Progressionsgraphen' },
]
const LEVEL_FILTER_OPTS = SKILL_LEVEL_OPTIONS.filter((o) => o.level != null)

const VIS_LABELS = { official: 'Global', club: 'Verein', private: 'Privat' }
const STATUS_LABELS = {
  draft: 'Entwurf',
  in_review: 'In Prüfung',
  approved: 'Freigegeben',
  archived: 'Archiv',
}

function visibilityLabel(v) {
  return VIS_LABELS[v] || v || '—'
}

function statusLabel(s) {
  return STATUS_LABELS[s] || s || '—'
}

function pushCatalogRuleFilterChips(chips, field, rules, options, topicLabel, setFilters) {
  ;(rules || []).forEach((r) => {
    const rid = String(r.id ?? r.focus_area_id ?? '')
    const opt = options.find((o) => String(o.id) === rid)
    chips.push({
      key: `${field}-${r.key}`,
      label: `${topicLabel}: ${r.mode === 'forbid' ? '−' : '+'} ${opt?.label ?? rid}`,
      onRemove: () =>
        setFilters((prev) => ({
          ...prev,
          [field]: (prev[field] || []).filter((x) => x.key !== r.key),
        })),
    })
  })
}

function exerciseFocusNames(ex) {
  const fromApi = coerceApiNameList(ex.focus_area_names)
  if (fromApi.length) return fromApi
  if (ex.focus_area) return [ex.focus_area]
  return []
}

function exerciseCardClassName(exercise, userId) {
  const vis = exercise.visibility || 'private'
  const visKey = vis === 'official' || vis === 'club' || vis === 'private' ? vis : 'private'
  const mine = userId != null && Number(exercise.created_by) === Number(userId)
  return ['card', 'exercise-card', `exercise-card--scope-${visKey}`, mine ? 'exercise-card--mine' : '']
    .filter(Boolean)
    .join(' ')
}

function ExerciseCardScopeStatus({ exercise }) {
  const v = exercise.visibility || 'private'
  const s = exercise.status || 'draft'
  const visLabel = visibilityLabel(v)
  const stLabel = statusLabel(s)
  const tip = `${visLabel} · ${stLabel}`
  let VisIcon = Lock
  if (v === 'official') VisIcon = Globe
  else if (v === 'club') VisIcon = Users
  let StatIcon = FilePenLine
  if (s === 'approved') StatIcon = CheckCircle2
  else if (s === 'archived') StatIcon = Archive
  else if (s === 'in_review') StatIcon = CircleDot
  return (
    <div
      className="exercise-card__meta-compact"
      title={tip}
      aria-label={`Sichtbarkeit: ${visLabel}. Status: ${stLabel}.`}
    >
      <span className="exercise-card__meta-glyph">
        <VisIcon size={15} strokeWidth={2} aria-hidden />
      </span>
      <span className="exercise-card__meta-sep" aria-hidden>
        ·
      </span>
      <span className="exercise-card__meta-glyph">
        <StatIcon size={15} strokeWidth={2} aria-hidden />
      </span>
    </div>
  )
}

function levelOptionShort(levelStr) {
  const o = LEVEL_FILTER_OPTS.find((x) => String(x.level) === String(levelStr))
  return o ? String(o.level) : String(levelStr)
}

function applyDashboardExerciseListUrl(mergedFromPrefs) {
  try {
    const sp = new URLSearchParams(window.location.search)
    const mine = sp.get('mine') === '1' || sp.get('created_by_me') === '1'
    const statusDraft = sp.get('status') === 'draft'

    if (mine) {
      const next = { ...INITIAL_EXERCISE_LIST_FILTERS }
      if (statusDraft) {
        next.status_rules = [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }]
      }
      return next
    }

    if (statusDraft) {
      return {
        ...mergedFromPrefs,
        status_rules: [{ key: 'url-dashboard-draft', id: 'draft', mode: 'require' }],
      }
    }
    return mergedFromPrefs
  } catch {
    return mergedFromPrefs
  }
}

function ExercisesListPage() {
  const { user, checkAuth } = useAuth()
  const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
  const isSuperadmin = user?.role === 'superadmin'

  const [mineOnly, setMineOnly] = useState(() => {
    try {
      const sp = new URLSearchParams(window.location.search)
      return sp.get('mine') === '1' || sp.get('created_by_me') === '1'
    } catch {
      return false
    }
  })

  const [exercises, setExercises] = useState([])
  const [catalogs, setCatalogs] = useState({
    focusAreas: [],
    styleDirections: [],
    trainingTypes: [],
    targetGroups: [],
    skills: [],
  })
  const [catalogsReady, setCatalogsReady] = useState(false)
  const [listFetching, setListFetching] = useState(false)
  const [loadingMore, setLoadingMore] = useState(false)
  const [offset, setOffset] = useState(0)
  const [hasMore, setHasMore] = useState(false)
  const [searchInput, setSearchInput] = useState('')
  const [aiSearchInput, setAiSearchInput] = useState('')
  const [debouncedSearch, setDebouncedSearch] = useState('')
  const [debouncedAiSearch, setDebouncedAiSearch] = useState('')
  const [filters, setFilters] = useState(() => ({ ...INITIAL_EXERCISE_LIST_FILTERS }))
  const [filterModalOpen, setFilterModalOpen] = useState(false)
  const [savingExercisePrefs, setSavingExercisePrefs] = useState(false)
  const [pageTab, setPageTab] = useState('list')
  const prefsAppliedRef = useRef(false)

  const [selectedIds, setSelectedIds] = useState(() => new Set())
  const [bulkModalOpen, setBulkModalOpen] = useState(false)
  const [bulkVisibility, setBulkVisibility] = useState('')
  const [bulkStatus, setBulkStatus] = useState('')
  const [bulkClubSelect, setBulkClubSelect] = useState('')
  const [bulkClubManual, setBulkClubManual] = useState('')
  const [bulkSubmitting, setBulkSubmitting] = useState(false)
  const [bulkPatchFocusAreas, setBulkPatchFocusAreas] = useState(false)
  const [bulkFocusAreaIds, setBulkFocusAreaIds] = useState([])
  const [bulkPatchStyleDirections, setBulkPatchStyleDirections] = useState(false)
  const [bulkStyleDirectionIds, setBulkStyleDirectionIds] = useState([])
  const [bulkPatchTrainingTypes, setBulkPatchTrainingTypes] = useState(false)
  const [bulkTrainingTypeIds, setBulkTrainingTypeIds] = useState([])
  const [bulkPatchTargetGroups, setBulkPatchTargetGroups] = useState(false)
  const [bulkTargetGroupIds, setBulkTargetGroupIds] = useState([])

  useEffect(() => {
    if (!user?.id) return
    if (prefsAppliedRef.current) return
    const merged = mergeExerciseListPrefsFromApi(user.exercise_list_prefs)
    setFilters(applyDashboardExerciseListUrl(merged))
    try {
      const sp = new URLSearchParams(window.location.search)
      if (sp.get('mine') === '1' || sp.get('created_by_me') === '1') setMineOnly(true)
    } catch {
      /* ignore */
    }
    prefsAppliedRef.current = true
  }, [user?.id, user?.exercise_list_prefs])

  useEffect(() => {
    if (!user?.id) prefsAppliedRef.current = false
  }, [user?.id])

  useEffect(() => {
    const t = setTimeout(() => setDebouncedSearch(searchInput.trim()), 400)
    return () => clearTimeout(t)
  }, [searchInput])

  useEffect(() => {
    const t = setTimeout(() => setDebouncedAiSearch(aiSearchInput.trim()), 400)
    return () => clearTimeout(t)
  }, [aiSearchInput])

  useEffect(() => {
    if (!filterModalOpen) return
    const onKey = (e) => {
      if (e.key === 'Escape') setFilterModalOpen(false)
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [filterModalOpen])

  const focusOptions = useMemo(
    () =>
      catalogs.focusAreas.map((fa) => ({
        id: fa.id,
        label: `${fa.icon || ''} ${fa.name || ''}`.trim(),
      })),
    [catalogs.focusAreas]
  )
  const styleOptions = useMemo(
    () => catalogs.styleDirections.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
    [catalogs.styleDirections]
  )
  const trainingTypeOptions = useMemo(
    () => catalogs.trainingTypes.map((t) => ({ id: t.id, label: t.name || String(t.id) })),
    [catalogs.trainingTypes]
  )
  const targetGroupOptions = useMemo(
    () => catalogs.targetGroups.map((g) => ({ id: g.id, label: g.name || String(g.id) })),
    [catalogs.targetGroups]
  )
  const skillOptions = useMemo(
    () => catalogs.skills.map((s) => ({ id: s.id, label: s.name || String(s.id) })),
    [catalogs.skills]
  )
  const visibilityOptions = useMemo(
    () => [
      { id: 'private', label: 'Privat' },
      { id: 'club', label: 'Verein' },
      { id: 'official', label: 'Offiziell' },
    ],
    []
  )
  const statusOptions = useMemo(
    () => [
      { id: 'draft', label: 'Entwurf' },
      { id: 'in_review', label: 'In Prüfung' },
      { id: 'approved', label: 'Freigegeben' },
      { id: 'archived', label: 'Archiviert' },
    ],
    []
  )

  const filterChips = useMemo(() => {
    const chips = []

    if (mineOnly) {
      chips.push({
        key: 'mine-only',
        label: 'Nur von mir erstellt',
        onRemove: () => setMineOnly(false),
      })
    }

    pushCatalogRuleFilterChips(chips, 'focus_rules', filters.focus_rules, focusOptions, 'Fokus', setFilters)

    if (filters.focus_only_without) {
      chips.push({
        key: 'focus-only-none',
        label: 'Nur ohne Fokusbereich',
        onRemove: () => setFilters((prev) => ({ ...prev, focus_only_without: false })),
      })
    }

    ;(filters.focus_area_ids || []).forEach((id) => {
      const opt = focusOptions.find((o) => String(o.id) === String(id))
      chips.push({
        key: `fa-${id}`,
        label: `Fokus (ODER, älter): ${opt?.label ?? id}`,
        onRemove: () =>
          setFilters((prev) => ({
            ...prev,
            focus_area_ids: prev.focus_area_ids.filter((x) => String(x) !== String(id)),
          })),
      })
    })

    pushCatalogRuleFilterChips(
      chips,
      'style_direction_rules',
      filters.style_direction_rules,
      styleOptions,
      'Stil',
      setFilters
    )
    pushCatalogRuleFilterChips(
      chips,
      'training_type_rules',
      filters.training_type_rules,
      trainingTypeOptions,
      'Trainingsstil',
      setFilters
    )
    pushCatalogRuleFilterChips(
      chips,
      'target_group_rules',
      filters.target_group_rules,
      targetGroupOptions,
      'Zielgruppe',
      setFilters
    )

    ;(filters.style_direction_ids || []).forEach((id) => {
      const opt = styleOptions.find((o) => String(o.id) === String(id))
      chips.push({
        key: `sd-${id}`,
        label: `Stil (ODER, älter): ${opt?.label ?? id}`,
        onRemove: () =>
          setFilters((prev) => ({
            ...prev,
            style_direction_ids: prev.style_direction_ids.filter((x) => String(x) !== String(id)),
          })),
      })
    })
    ;(filters.training_type_ids || []).forEach((id) => {
      const opt = trainingTypeOptions.find((o) => String(o.id) === String(id))
      chips.push({
        key: `tt-${id}`,
        label: `Trainingsstil (ODER, älter): ${opt?.label ?? id}`,
        onRemove: () =>
          setFilters((prev) => ({
            ...prev,
            training_type_ids: prev.training_type_ids.filter((x) => String(x) !== String(id)),
          })),
      })
    })
    ;(filters.target_group_ids || []).forEach((id) => {
      const opt = targetGroupOptions.find((o) => String(o.id) === String(id))
      chips.push({
        key: `tg-${id}`,
        label: `Zielgruppe (ODER, älter): ${opt?.label ?? id}`,
        onRemove: () =>
          setFilters((prev) => ({
            ...prev,
            target_group_ids: prev.target_group_ids.filter((x) => String(x) !== String(id)),
          })),
      })
    })

    ;(filters.skill_ids || []).forEach((id) => {
      const opt = skillOptions.find((o) => String(o.id) === String(id))
      chips.push({
        key: `sk-${id}`,
        label: `Fähigkeit: ${opt?.label ?? id}`,
        onRemove: () =>
          setFilters((prev) => ({
            ...prev,
            skill_ids: prev.skill_ids.filter((x) => String(x) !== String(id)),
          })),
      })
    })

    if (filters.skill_min_level || filters.skill_max_level) {
      const a = filters.skill_min_level ? levelOptionShort(filters.skill_min_level) : '…'
      const b = filters.skill_max_level ? levelOptionShort(filters.skill_max_level) : '…'
      chips.push({
        key: 'skill-levels',
        label: `Stufe ${a}–${b}`,
        onRemove: () =>
          setFilters((prev) => ({
            ...prev,
            skill_min_level: '',
            skill_max_level: '',
          })),
      })
    }

    pushCatalogRuleFilterChips(
      chips,
      'visibility_rules',
      filters.visibility_rules,
      visibilityOptions,
      'Sichtbarkeit',
      setFilters
    )
    pushCatalogRuleFilterChips(chips, 'status_rules', filters.status_rules, statusOptions, 'Status', setFilters)

    if (filters.exclude_without_focus) {
      chips.push({
        key: 'ex-no-focus',
        label: 'Ohne Fokus ausblenden',
        onRemove: () => setFilters((prev) => ({ ...prev, exclude_without_focus: false })),
      })
    }
    if (filters.include_archived) {
      chips.push({
        key: 'inc-arch',
        label: 'Archivierte anzeigen',
        onRemove: () => setFilters((prev) => ({ ...prev, include_archived: false })),
      })
    }

    return chips
  }, [
    mineOnly,
    filters,
    focusOptions,
    styleOptions,
    trainingTypeOptions,
    targetGroupOptions,
    skillOptions,
    visibilityOptions,
    statusOptions,
    setFilters,
  ])

  /** Für Browser-/datalist-Vorschläge (aktuelle Treffer-Titel, begrenzt) */
  const searchTitleSuggestions = useMemo(() => {
    const titles = exercises.map((e) => (e.title || '').trim()).filter(Boolean)
    return [...new Set(titles)].slice(0, 80)
  }, [exercises])

  const queryBase = useMemo(() => {
    const q = {}
    const n = (v) => (v === '' || v == null ? undefined : Number(v))
    const ids = (arr) =>
      Array.isArray(arr) && arr.length ? arr.map((x) => Number(x)).filter((x) => !Number.isNaN(x)) : undefined
    const fMn = splitMnCatalogRules(filters.focus_rules)
    if (fMn.includeIds.length) q.focus_area_must_include_ids = fMn.includeIds
    if (fMn.excludeIds.length) q.focus_area_must_exclude_ids = fMn.excludeIds
    if (filters.focus_only_without) q.focus_only_without_focus_areas = true

    const fa = ids(filters.focus_area_ids)
    if (fa?.length) q.focus_area_ids = fa

    const sdMn = splitMnCatalogRules(filters.style_direction_rules)
    if (sdMn.includeIds.length) q.style_direction_must_include_ids = sdMn.includeIds
    if (sdMn.excludeIds.length) q.style_direction_must_exclude_ids = sdMn.excludeIds
    const sdLegacy = ids(filters.style_direction_ids)
    if (sdLegacy?.length) q.style_direction_ids = sdLegacy

    const ttMn = splitMnCatalogRules(filters.training_type_rules)
    if (ttMn.includeIds.length) q.training_type_must_include_ids = ttMn.includeIds
    if (ttMn.excludeIds.length) q.training_type_must_exclude_ids = ttMn.excludeIds
    const ttLegacy = ids(filters.training_type_ids)
    if (ttLegacy?.length) q.training_type_ids = ttLegacy

    const tgMn = splitMnCatalogRules(filters.target_group_rules)
    if (tgMn.includeIds.length) q.target_group_must_include_ids = tgMn.includeIds
    if (tgMn.excludeIds.length) q.target_group_must_exclude_ids = tgMn.excludeIds
    const tgLegacy = ids(filters.target_group_ids)
    if (tgLegacy?.length) q.target_group_ids = tgLegacy

    const visMn = splitScalarCatalogRules(filters.visibility_rules)
    if (visMn.includeVals.length) q.visibility_any = visMn.includeVals
    if (visMn.excludeVals.length) q.visibility_exclude_any = visMn.excludeVals

    const stMn = splitScalarCatalogRules(filters.status_rules)
    if (stMn.includeVals.length) q.status_any = stMn.includeVals
    if (stMn.excludeVals.length) q.status_exclude_any = stMn.excludeVals

    const sk = ids(filters.skill_ids)
    if (sk?.length) q.skill_ids = sk
    if (filters.skill_min_level) q.skill_min_level = n(filters.skill_min_level)
    if (filters.skill_max_level) q.skill_max_level = n(filters.skill_max_level)
    if (filters.exclude_without_focus) q.exclude_without_focus = true
    if (filters.include_archived) q.include_archived = true
    if (debouncedSearch) q.search = debouncedSearch
    if (debouncedAiSearch) q.ai_search = debouncedAiSearch
    if (mineOnly) q.created_by_me = true
    return q
  }, [filters, debouncedSearch, debouncedAiSearch, mineOnly])

  useEffect(() => {
    setSelectedIds(new Set())
  }, [queryBase])

  const clubNameById = useMemo(() => {
    const m = {}
    for (const c of activeClubMemberships(user?.clubs)) {
      if (c?.id != null) m[Number(c.id)] = c.name || `#${c.id}`
    }
    return m
  }, [user?.clubs])

  const effectiveClubId =
    user?.effective_club_id != null && user.effective_club_id !== ''
      ? Number(user.effective_club_id)
      : user?.active_club_id != null && user.active_club_id !== ''
        ? Number(user.active_club_id)
        : null

  const toggleSelect = useCallback((id) => {
    setSelectedIds((prev) => {
      const n = new Set(prev)
      const nid = Number(id)
      if (Number.isNaN(nid)) return prev
      if (n.has(nid)) n.delete(nid)
      else n.add(nid)
      return n
    })
  }, [])

  const clearSelection = useCallback(() => setSelectedIds(new Set()), [])

  const toggleSelectAllPage = useCallback(() => {
    setSelectedIds((prev) => {
      const n = new Set(prev)
      const allSel =
        exercises.length > 0 && exercises.every((e) => n.has(Number(e.id)))
      if (allSel) {
        exercises.forEach((e) => n.delete(Number(e.id)))
      } else {
        exercises.forEach((e) => n.add(Number(e.id)))
      }
      return n
    })
  }, [exercises])

  const allOnPageSelected = useMemo(
    () => exercises.length > 0 && exercises.every((e) => selectedIds.has(Number(e.id))),
    [exercises, selectedIds]
  )

  const bulkVisibilityOptions = useMemo(() => {
    const base = [
      { id: '', label: '— nicht ändern —' },
      { id: 'private', label: 'Privat' },
      { id: 'club', label: 'Verein' },
    ]
    if (isSuperadmin) base.push({ id: 'official', label: 'Offiziell (global)' })
    return base
  }, [isSuperadmin])

  useEffect(() => {
    let cancelled = false
    ;(async () => {
      try {
        const [fa, sd, tt, tg, sk] = await Promise.all([
          api.listFocusAreas(),
          api.listStyleDirections(),
          api.listTrainingTypes(),
          api.listTargetGroups(),
          api.listSkills(),
        ])
        if (!cancelled) {
          setCatalogs({
            focusAreas: fa,
            styleDirections: sd,
            trainingTypes: tt,
            targetGroups: tg,
            skills: sk,
          })
          setCatalogsReady(true)
        }
      } catch (err) {
        if (!cancelled) {
          console.error(err)
          alert('Kataloge konnten nicht geladen werden: ' + err.message)
          setCatalogsReady(true)
        }
      }
    })()
    return () => {
      cancelled = true
    }
  }, [])

  useEffect(() => {
    if (!catalogsReady || pageTab !== 'list') return
    let cancelled = false
    const run = async () => {
      setListFetching(true)
      setOffset(0)
      try {
        const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset: 0 })
        if (cancelled) return
        setExercises(batch)
        setHasMore(batch.length === PAGE_SIZE)
        setOffset(batch.length)
      } catch (err) {
        if (!cancelled) {
          console.error('Failed to load data:', err)
          alert('Fehler beim Laden: ' + err.message)
        }
      } finally {
        if (!cancelled) setListFetching(false)
      }
    }
    run()
    return () => {
      cancelled = true
    }
  }, [queryBase, catalogsReady, pageTab])

  const loadMore = async () => {
    if (loadingMore || !hasMore) return
    setLoadingMore(true)
    try {
      const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset })
      setExercises((prev) => [...prev, ...batch])
      setHasMore(batch.length === PAGE_SIZE)
      setOffset((o) => o + batch.length)
    } catch (err) {
      alert('Fehler: ' + err.message)
    } finally {
      setLoadingMore(false)
    }
  }

  const handleDelete = async (exercise) => {
    if (!confirm(`Übung "${exercise.title}" wirklich löschen?`)) return
    try {
      await api.deleteExercise(exercise.id)
      setExercises((prev) => prev.filter((e) => e.id !== exercise.id))
    } catch (err) {
      alert('Fehler beim Löschen: ' + err.message)
    }
  }

  const resetAllFilters = useCallback(() => {
    setMineOnly(false)
    setFilters({ ...INITIAL_EXERCISE_LIST_FILTERS })
  }, [])

  const handleSaveExerciseFilterPrefs = useCallback(async () => {
    const uid = user?.id
    if (!uid) {
      alert('Nicht angemeldet.')
      return
    }
    setSavingExercisePrefs(true)
    try {
      const payload = compactExerciseListPrefsPayload(filters)
      await api.updateProfile(uid, { exercise_list_prefs: payload })
      await checkAuth()
      alert('Standardfilter für die Übungsliste gespeichert.')
    } catch (e) {
      alert('Speichern fehlgeschlagen: ' + (e.message || String(e)))
    } finally {
      setSavingExercisePrefs(false)
    }
  }, [user?.id, filters, checkAuth])

  const openBulkModal = () => {
    setBulkVisibility('')
    setBulkStatus('')
    setBulkClubSelect('')
    setBulkClubManual('')
    setBulkPatchFocusAreas(false)
    setBulkFocusAreaIds([])
    setBulkPatchStyleDirections(false)
    setBulkStyleDirectionIds([])
    setBulkPatchTrainingTypes(false)
    setBulkTrainingTypeIds([])
    setBulkPatchTargetGroups(false)
    setBulkTargetGroupIds([])
    setBulkModalOpen(true)
  }

  const handleBulkSubmit = async () => {
    const anyRelationPatch =
      bulkPatchFocusAreas ||
      bulkPatchStyleDirections ||
      bulkPatchTrainingTypes ||
      bulkPatchTargetGroups
    if (!bulkVisibility && !bulkStatus && !anyRelationPatch) {
      alert(
        'Bitte mindestens eine Änderung wählen (Sichtbarkeit, Status oder eine der Zuordnungen mit gesetztem Häkchen „ersetzen“).'
      )
      return
    }
    const ids = Array.from(selectedIds).filter((x) => Number.isFinite(x) && x > 0)
    if (ids.length === 0) {
      alert('Keine Übungen ausgewählt.')
      return
    }
    if (ids.length > BULK_MAX_IDS) {
      alert(`Maximal ${BULK_MAX_IDS} Übungen pro Vorgang. Bitte Auswahl oder mehrere Durchläufe verwenden.`)
      return
    }
    const payload = { exercise_ids: ids }
    if (bulkVisibility) payload.visibility = bulkVisibility
    if (bulkStatus) payload.status = bulkStatus
    if (bulkPatchFocusAreas) {
      payload.focus_area_ids = bulkFocusAreaIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
    }
    if (bulkPatchStyleDirections) {
      payload.style_direction_ids = bulkStyleDirectionIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
    }
    if (bulkPatchTrainingTypes) {
      payload.training_type_ids = bulkTrainingTypeIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
    }
    if (bulkPatchTargetGroups) {
      payload.target_group_ids = bulkTargetGroupIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
    }
    if (bulkVisibility === 'club') {
      const manual = String(bulkClubManual || '').trim()
      if (manual && /^\d+$/.test(manual)) payload.club_id = Number(manual)
      else if (bulkClubSelect && /^\d+$/.test(String(bulkClubSelect))) {
        payload.club_id = Number(bulkClubSelect)
      }
    }
    setBulkSubmitting(true)
    try {
      const res = await api.bulkPatchExercisesMetadata(payload)
      const updatedSet = new Set((res.updated || []).map((x) => Number(x)))
      let resolvedClubId = null
      if (bulkVisibility === 'club') {
        if (payload.club_id != null) resolvedClubId = payload.club_id
        else if (effectiveClubId != null && !Number.isNaN(effectiveClubId)) resolvedClubId = effectiveClubId
      }
      const clubLabel =
        resolvedClubId != null ? clubNameById[resolvedClubId] || `Verein #${resolvedClubId}` : null

      let nextPrimaryFocusName = null
      if (bulkPatchFocusAreas) {
        const faNums = bulkFocusAreaIds.map((x) => Number(x)).filter((n) => Number.isFinite(n) && n > 0)
        if (faNums.length > 0) {
          const opt = focusOptions.find((o) => Number(o.id) === Number(faNums[0]))
          nextPrimaryFocusName = String(opt?.label ?? '').trim() || String(faNums[0])
        }
      }

      setExercises((prev) =>
        prev.map((e) => {
          if (!updatedSet.has(Number(e.id))) return e
          const next = { ...e }
          if (bulkVisibility) {
            next.visibility = bulkVisibility
            next.club_id = bulkVisibility === 'club' ? resolvedClubId : null
            next.club_name = bulkVisibility === 'club' ? clubLabel : null
          }
          if (bulkStatus) next.status = bulkStatus
          if (bulkPatchFocusAreas) {
            if (nextPrimaryFocusName == null) delete next.focus_area
            else next.focus_area = nextPrimaryFocusName
          }
          return next
        })
      )

      let msg = `${res.updated_count ?? updatedSet.size} Übung(en) aktualisiert.`
      if (res.failed_count) msg += `\n${res.failed_count} nicht geändert (siehe Details).`
      if (Array.isArray(res.failed) && res.failed.length) {
        msg +=
          '\n\n' +
          res.failed
            .slice(0, 12)
            .map((f) => `#${f.id}: ${f.detail}`)
            .join('\n')
        if (res.failed.length > 12) msg += '\n…'
      }
      alert(msg)
      setBulkModalOpen(false)
      clearSelection()
    } catch (err) {
      alert('Massenänderung fehlgeschlagen: ' + (err.message || String(err)))
    } finally {
      setBulkSubmitting(false)
    }
  }

  if (!catalogsReady && pageTab === 'list') {
    return (
      <div className="app-page">
        <div className="empty-state" style={{ padding: '2.5rem 1rem' }}>
          <div className="spinner" />
          <p className="muted" style={{ marginTop: '12px' }}>
            Lade Kataloge…
          </p>
        </div>
      </div>
    )
  }

  return (
    <div className="app-page">
      <div className="exercises-page__header">
        <h1 className="page-title exercises-page__title">Übungen</h1>
        {pageTab === 'list' ? (
          <Link to="/exercises/new" className="btn btn-primary">
            + Neu
          </Link>
        ) : (
          <span aria-hidden="true" />
        )}
      </div>

      <PageSectionNav
        ariaLabel="Übungen Bereiche"
        value={pageTab}
        onChange={setPageTab}
        items={EXERCISES_PAGE_TABS}
        className="exercises-page-toolbar-tabs"
      />

      {pageTab === 'progression' ? (
        <ExerciseProgressionGraphPanel />
      ) : (
        <>
      <div className="card exercise-search-bar">
        <label className="form-label">Volltextsuche (Titel, Ziel, …)</label>
        <datalist id="exercise-search-titles">
          {searchTitleSuggestions.map((t) => (
            <option key={t} value={t} />
          ))}
        </datalist>
        <input
          type="search"
          className="form-input exercise-search-bar__primary"
          placeholder="Suchbegriffe…"
          value={searchInput}
          onChange={(e) => setSearchInput(e.target.value)}
          autoComplete="on"
          name="exercise-fulltext-search"
          list="exercise-search-titles"
          enterKeyHint="search"
        />
        <label className="form-label">Ergänzende Suche / KI-Vorbereitung (Beta)</label>
        <input
          type="search"
          className="form-input"
          placeholder="zweiter Begriff — zusätzliche Volltextsuche (ODER)"
          value={aiSearchInput}
          onChange={(e) => setAiSearchInput(e.target.value)}
          autoComplete="on"
          name="exercise-ai-search"
          list="exercise-search-titles"
          enterKeyHint="search"
        />
        <div className="exercise-search-bar__actions exercise-search-bar__actions--split">
          <div className="exercise-search-bar__actions-main">
            <button
              type="button"
              className={'btn btn-secondary exercise-mine-toggle' + (mineOnly ? ' exercise-mine-toggle--active' : '')}
              onClick={() => setMineOnly((v) => !v)}
              title="Nur Übungen, die mit deinem Profil als Ersteller gespeichert sind"
            >
              Meine Übungen
            </button>
            <button type="button" className="btn btn-secondary exercise-filter-trigger" onClick={() => setFilterModalOpen(true)}>
              Filter
              {filterChips.length > 0 ? (
                <span className="exercise-filter-badge" aria-hidden>
                  {filterChips.length}
                </span>
              ) : null}
            </button>
            {filterChips.length > 0 ? (
              <button type="button" className="btn" onClick={resetAllFilters}>
                Alle entfernen
              </button>
            ) : null}
          </div>
        </div>
        {filterChips.length > 0 ? (
          <div className="exercise-filter-chips-row" role="list" aria-label="Aktive Filter">
            {filterChips.map((c) => (
              <button
                key={c.key}
                type="button"
                role="listitem"
                className="exercise-filter-chip"
                title="Filter entfernen"
                onClick={() => c.onRemove()}
              >
                <span className="exercise-filter-chip__text">{c.label}</span>
                <span className="exercise-filter-chip__x" aria-hidden>
                  ×
                </span>
              </button>
            ))}
          </div>
        ) : null}
        <p className="exercise-search-hint">
          Trefferliste aktualisiert sich kurz nach Eingabe. Titel der aktuellen Liste erscheinen als Vorschläge (Pfeil im
          Feld). Fachliche Filter über „Filter“ – zwischen Feldern UND, Auswahl mehrerer Werte je Feld mit ODER.
          {exercises.length > 0 ? (
            <>
              {' '}
              <button type="button" className="btn btn-secondary btn-small" onClick={toggleSelectAllPage}>
                {allOnPageSelected ? 'Auswahl auf dieser Seite aufheben' : 'Alle auf dieser Seite auswählen'}
              </button>
            </>
          ) : null}
        </p>
      </div>

      {selectedIds.size > 0 ? (
        <div className="card exercise-bulk-toolbar">
          <strong>{selectedIds.size} ausgewählt</strong>
          <button type="button" className="btn btn-secondary btn-small" onClick={clearSelection}>
            Auswahl aufheben
          </button>
          <button type="button" className="btn btn-primary btn-small" onClick={openBulkModal}>
            Massenänderung…
          </button>
          <span className="exercise-bulk-toolbar__meta">
            Bis zu {BULK_MAX_IDS} pro Anfrage. Für „Verein“ ohne Auswahl: aktiver Vereinskontext (
            <code>X-Active-Club-Id</code>
            ).
          </span>
        </div>
      ) : null}

      {filterModalOpen && (
        <div
          className="admin-modal-backdrop"
          role="presentation"
          onClick={(e) => {
            if (e.target === e.currentTarget) setFilterModalOpen(false)
          }}
        >
          <div
            className="admin-modal-sheet exercise-filter-modal"
            role="dialog"
            aria-modal="true"
            aria-labelledby="exercise-filter-modal-title"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="admin-modal-sheet__header">
              <h3 id="exercise-filter-modal-title" className="admin-modal-sheet__title">
                Übungen filtern
              </h3>
              <button
                type="button"
                className="btn btn-secondary admin-modal-sheet__close"
                onClick={() => setFilterModalOpen(false)}
              >
                Schließen
              </button>
            </div>
            <div className="admin-modal-sheet__body exercise-filter-modal__scroll">
              <p className="muted" style={{ marginTop: 0, marginBottom: '14px' }}>
                Zwischen den Bereichen gilt <strong>UND</strong>. Fokusbereiche: mehrere „+ mit“ bedeuten alle müssen
                gesetzt sein; „− ohne“ schließt Übungen aus, die diesen Fokus zusätzlich haben. Stilrichtung /
                Trainingsstil / Zielgruppe: mehrere „+“ = alle zutreffend (UND); „−“ verbietet die Zuordnung. Unter
                „Freigabe“: Sichtbarkeit / Status mit „+“ = eine davon (ODER); „−“ blendet aus.
              </p>

              <section className="exercise-filter-section">
                <h4 className="exercise-filter-section-title">Zuordnung</h4>
                <ExerciseFocusRulePicker
                  focusOptions={focusOptions}
                  focusRules={filters.focus_rules}
                  focusOnlyWithout={filters.focus_only_without}
                  legacyFocusAreaIds={filters.focus_area_ids}
                  onPatch={(patch) => setFilters((prev) => ({ ...prev, ...patch }))}
                />
                <div className="exercise-filters-modal-grid exercise-filters-modal-grid--catalog" style={{ marginTop: '12px' }}>
                  <CatalogRulePicker
                    label="Stilrichtung"
                    hint="+ alle nötig (UND). − verbietet Zuordnung."
                    options={styleOptions}
                    rules={filters.style_direction_rules}
                    rulesFieldName="style_direction_rules"
                    placeholder="Stil …"
                    onPatch={(patch) => setFilters((prev) => ({ ...prev, ...patch }))}
                  />
                  <CatalogRulePicker
                    label="Trainingsstil"
                    hint="+ alle nötig (UND). − verbietet Zuordnung."
                    options={trainingTypeOptions}
                    rules={filters.training_type_rules}
                    rulesFieldName="training_type_rules"
                    placeholder="Trainingsstil …"
                    onPatch={(patch) => setFilters((prev) => ({ ...prev, ...patch }))}
                  />
                  <CatalogRulePicker
                    label="Zielgruppe"
                    hint="+ alle nötig (UND). − verbietet Zuordnung."
                    options={targetGroupOptions}
                    rules={filters.target_group_rules}
                    rulesFieldName="target_group_rules"
                    placeholder="Gruppe …"
                    onPatch={(patch) => setFilters((prev) => ({ ...prev, ...patch }))}
                  />
                </div>
              </section>

              <section className="exercise-filter-section">
                <h4 className="exercise-filter-section-title">Fähigkeit und zugehörige Stufe</h4>
                <div className="exercise-filter-skill-block">
                  <label className="form-label">Fähigkeit</label>
                  <MultiSelectCombo
                    value={filters.skill_ids}
                    onChange={(v) => setFilters({ ...filters, skill_ids: v })}
                    options={skillOptions}
                    placeholder="Fähigkeit suchen …"
                  />
                  <p className="exercise-filter-skill-hint">
                    Die Stufen filtern nach dem Niveau der Zuordnung Übung ↔ Fähigkeit (von–bis).
                  </p>
                  <div className="exercise-filter-skill-levels-row">
                    <label className="exercise-filter-skill-level-field">
                      <span className="exercise-filter-skill-level-caption">von</span>
                      <select
                        className="form-input exercise-filter-level-select"
                        title="Mindest-Stufe"
                        value={filters.skill_min_level}
                        onChange={(e) => setFilters({ ...filters, skill_min_level: e.target.value })}
                      >
                        <option value="">–</option>
                        {LEVEL_FILTER_OPTS.map((o) => (
                          <option key={o.value} value={String(o.level)} title={o.label}>
                            {o.level}
                          </option>
                        ))}
                      </select>
                    </label>
                    <span className="exercise-filter-skill-dash" aria-hidden>
                      –
                    </span>
                    <label className="exercise-filter-skill-level-field">
                      <span className="exercise-filter-skill-level-caption">bis</span>
                      <select
                        className="form-input exercise-filter-level-select"
                        title="Höchst-Stufe"
                        value={filters.skill_max_level}
                        onChange={(e) => setFilters({ ...filters, skill_max_level: e.target.value })}
                      >
                        <option value="">–</option>
                        {LEVEL_FILTER_OPTS.map((o) => (
                          <option key={`m-${o.value}`} value={String(o.level)} title={o.label}>
                            {o.level}
                          </option>
                        ))}
                      </select>
                    </label>
                  </div>
                </div>
              </section>

              <section className="exercise-filter-section">
                <h4 className="exercise-filter-section-title">Ausblenden / Liste</h4>
                <p className="muted" style={{ marginTop: 0, marginBottom: '12px', fontSize: '13px' }}>
                  Sichtbarkeit und Status steuern Sie unter „Freigabe“ mit + und −. Hier nur globale Listen-Optionen.
                </p>
                <div style={{ marginTop: '6px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
                  <label
                    style={{
                      display: 'flex',
                      alignItems: 'center',
                      gap: '10px',
                      cursor: filters.focus_only_without ? 'not-allowed' : 'pointer',
                      opacity: filters.focus_only_without ? 0.55 : 1,
                    }}
                  >
                    <input
                      type="checkbox"
                      disabled={!!filters.focus_only_without}
                      checked={!!filters.exclude_without_focus}
                      onChange={(e) =>
                        setFilters((prev) => ({
                          ...prev,
                          exclude_without_focus: e.target.checked,
                          ...(e.target.checked ? { focus_only_without: false } : {}),
                        }))
                      }
                    />
                    <span>Übungen ohne Fokusbereich ausblenden</span>
                  </label>
                  <label style={{ display: 'flex', alignItems: 'center', gap: '10px', cursor: 'pointer' }}>
                    <input
                      type="checkbox"
                      checked={!!filters.include_archived}
                      onChange={(e) => setFilters({ ...filters, include_archived: e.target.checked })}
                    />
                    <span>Archivierte Übungen einblenden (ohne Haken werden sie standardmäßig ausgeblendet)</span>
                  </label>
                </div>
              </section>

              <section className="exercise-filter-section exercise-filter-section--last">
                <h4 className="exercise-filter-section-title">Freigabe</h4>
                <p className="muted" style={{ marginTop: 0, marginBottom: '10px', fontSize: '12px' }}>
                  Pro Übung nur ein Wert: mehrere „+“ bedeuten „eine davon“ (ODER). „−“ blendet Werte aus.
                </p>
                <div className="exercise-filters-modal-grid exercise-filters-modal-grid--two exercise-filters-modal-grid--catalog">
                  <CatalogRulePicker
                    label="Sichtbarkeit"
                    options={visibilityOptions}
                    rules={filters.visibility_rules}
                    rulesFieldName="visibility_rules"
                    idKind="string"
                    placeholder="Sichtbarkeit …"
                    onPatch={(patch) => setFilters((prev) => ({ ...prev, ...patch }))}
                  />
                  <CatalogRulePicker
                    label="Status"
                    options={statusOptions}
                    rules={filters.status_rules}
                    rulesFieldName="status_rules"
                    idKind="string"
                    placeholder="Status …"
                    onPatch={(patch) => setFilters((prev) => ({ ...prev, ...patch }))}
                  />
                </div>
              </section>
            </div>
            <div className="exercise-filter-modal__footer">
              <button type="button" className="btn btn-secondary" disabled={savingExercisePrefs} onClick={handleSaveExerciseFilterPrefs}>
                {savingExercisePrefs ? 'Speichern…' : 'Als Standard speichern'}
              </button>
              <button type="button" className="btn" onClick={resetAllFilters}>
                Alle Filter zurücksetzen
              </button>
              <button type="button" className="btn btn-primary" onClick={() => setFilterModalOpen(false)}>
                Fertig
              </button>
            </div>
          </div>
        </div>
      )}

      {bulkModalOpen ? (
        <div
          className="admin-modal-backdrop"
          role="presentation"
          onClick={(e) => {
            if (e.target === e.currentTarget) setBulkModalOpen(false)
          }}
        >
          <div
            className="admin-modal-sheet exercise-filter-modal"
            role="dialog"
            aria-modal="true"
            aria-labelledby="exercise-bulk-modal-title"
            onClick={(e) => e.stopPropagation()}
          >
            <div className="admin-modal-sheet__header">
              <h3 id="exercise-bulk-modal-title" className="admin-modal-sheet__title">
                Massenänderung
              </h3>
              <button
                type="button"
                className="btn btn-secondary admin-modal-sheet__close"
                onClick={() => setBulkModalOpen(false)}
              >
                Schließen
              </button>
            </div>
            <div className="admin-modal-sheet__body exercise-filter-modal__scroll">
              <p className="muted" style={{ marginTop: 0 }}>
                Es werden <strong>{selectedIds.size}</strong> Übung(en) aus der aktuellen Auswahl bearbeitet. Pro Durchlauf
                höchstens {BULK_MAX_IDS}. Ohne Berechtigung bleiben Einzelübungen unverändert (siehe Hinweis nach dem
                Speichern).
              </p>
              <p className="form-sub" style={{ marginTop: 0, marginBottom: '14px' }}>
                Unter „Zuordnung ersetzen“: die gewählte Liste ersetzt die bisherige Zuordnung bei allen betroffenen
                Übungen vollständig (leere Auswahl = alle Zuordnungen dieser Kategorie entfernen). Die erste Auswahl gilt
                als Primärzuordnung.
              </p>
              <div className="form-row">
                <label className="form-label">Sichtbarkeit</label>
                <select
                  className="form-input"
                  value={bulkVisibility}
                  onChange={(e) => setBulkVisibility(e.target.value)}
                >
                  {bulkVisibilityOptions.map((o) => (
                    <option key={o.id === '' ? '_unchanged' : o.id} value={o.id}>
                      {o.label}
                    </option>
                  ))}
                </select>
              </div>
              {bulkVisibility === 'club' ? (
                <div className="form-row">
                  <label className="form-label">Verein zuordnen</label>
                  <select
                    className="form-input"
                    value={bulkClubSelect}
                    onChange={(e) => setBulkClubSelect(e.target.value)}
                  >
                    <option value="">Aktiver Verein (Vereins-Umschalter / Header)</option>
                    {activeClubMemberships(user?.clubs).map((c) => (
                      <option key={c.id} value={String(c.id)}>
                        {c.name || `#${c.id}`}
                      </option>
                    ))}
                  </select>
                  {isPlatformAdmin ? (
                    <>
                      <label className="form-label" style={{ marginTop: '10px' }}>
                        Oder Vereins-ID (Plattform-Admin)
                      </label>
                      <input
                        type="number"
                        min={1}
                        className="form-input"
                        placeholder="Leer = wie Dropdown / aktiver Verein"
                        value={bulkClubManual}
                        onChange={(e) => setBulkClubManual(e.target.value)}
                      />
                    </>
                  ) : null}
                </div>
              ) : null}
              <div className="form-row">
                <label className="form-label">Status</label>
                <select
                  className="form-input"
                  value={bulkStatus}
                  onChange={(e) => setBulkStatus(e.target.value)}
                >
                  <option value="">— nicht ändern —</option>
                  {statusOptions.map((o) => (
                    <option key={o.id} value={o.id}>
                      {o.label}
                    </option>
                  ))}
                </select>
              </div>

              <section className="exercise-filter-section" style={{ marginTop: '8px', paddingTop: '12px' }}>
                <h4 className="exercise-filter-section-title">Zuordnung (optional)</h4>
                <div className="exercise-filters-modal-grid">
                  <div>
                    <label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                      <input
                        type="checkbox"
                        checked={bulkPatchFocusAreas}
                        onChange={(e) => {
                          const on = e.target.checked
                          setBulkPatchFocusAreas(on)
                          if (!on) setBulkFocusAreaIds([])
                        }}
                      />
                      Fokusbereiche ersetzen
                    </label>
                    {bulkPatchFocusAreas ? (
                      <MultiSelectCombo
                        value={bulkFocusAreaIds}
                        onChange={setBulkFocusAreaIds}
                        options={focusOptions}
                        placeholder="Fokusbereiche wählen …"
                      />
                    ) : null}
                  </div>
                  <div>
                    <label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                      <input
                        type="checkbox"
                        checked={bulkPatchStyleDirections}
                        onChange={(e) => {
                          const on = e.target.checked
                          setBulkPatchStyleDirections(on)
                          if (!on) setBulkStyleDirectionIds([])
                        }}
                      />
                      Stilrichtungen ersetzen
                    </label>
                    {bulkPatchStyleDirections ? (
                      <MultiSelectCombo
                        value={bulkStyleDirectionIds}
                        onChange={setBulkStyleDirectionIds}
                        options={styleOptions}
                        placeholder="Stilrichtungen wählen …"
                      />
                    ) : null}
                  </div>
                  <div>
                    <label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                      <input
                        type="checkbox"
                        checked={bulkPatchTrainingTypes}
                        onChange={(e) => {
                          const on = e.target.checked
                          setBulkPatchTrainingTypes(on)
                          if (!on) setBulkTrainingTypeIds([])
                        }}
                      />
                      Trainingsstile ersetzen
                    </label>
                    {bulkPatchTrainingTypes ? (
                      <MultiSelectCombo
                        value={bulkTrainingTypeIds}
                        onChange={setBulkTrainingTypeIds}
                        options={trainingTypeOptions}
                        placeholder="Trainingsstile wählen …"
                      />
                    ) : null}
                  </div>
                  <div>
                    <label className="form-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
                      <input
                        type="checkbox"
                        checked={bulkPatchTargetGroups}
                        onChange={(e) => {
                          const on = e.target.checked
                          setBulkPatchTargetGroups(on)
                          if (!on) setBulkTargetGroupIds([])
                        }}
                      />
                      Zielgruppen ersetzen
                    </label>
                    {bulkPatchTargetGroups ? (
                      <MultiSelectCombo
                        value={bulkTargetGroupIds}
                        onChange={setBulkTargetGroupIds}
                        options={targetGroupOptions}
                        placeholder="Zielgruppen wählen …"
                      />
                    ) : null}
                  </div>
                </div>
              </section>
            </div>
            <div className="exercise-filter-modal__footer">
              <button
                type="button"
                className="btn"
                disabled={bulkSubmitting}
                onClick={() => setBulkModalOpen(false)}
              >
                Abbrechen
              </button>
              <button type="button" className="btn btn-primary" disabled={bulkSubmitting} onClick={handleBulkSubmit}>
                {bulkSubmitting ? 'Speichern…' : 'Anwenden'}
              </button>
            </div>
          </div>
        </div>
      ) : null}

      {listFetching && exercises.length === 0 ? (
        <div className="card empty-state" style={{ padding: '2rem 1rem' }}>
          <div className="spinner" />
          <p className="muted" style={{ marginTop: '12px' }}>
            Lade Übungen…
          </p>
        </div>
      ) : exercises.length === 0 ? (
        <div className="card">
          <p className="exercises-empty-text">Keine Übungen gefunden.</p>
        </div>
      ) : (
        <>
          {listFetching ? (
            <p className="exercises-meta-line exercises-meta-line--muted">Aktualisiere Treffer…</p>
          ) : null}
          <p className="exercises-meta-line">
            {exercises.length} angezeigt
            {hasMore ? ' · es gibt weitere Einträge' : ''}
          </p>
          <div className="exercises-list-grid">
            {exercises.map((exercise) => {
              const focusNames = exerciseFocusNames(exercise)
              const styleNames = coerceApiNameList(exercise.style_direction_names)
              const typeNames = coerceApiNameList(exercise.training_type_names)
              return (
                <div key={exercise.id} className={exerciseCardClassName(exercise, user?.id)}>
                  <div className="exercise-card-layout exercise-card-layout--grow">
                    <input
                      type="checkbox"
                      checked={selectedIds.has(Number(exercise.id))}
                      onChange={() => toggleSelect(exercise.id)}
                      aria-label={`„${(exercise.title || 'Übung').replace(/"/g, '')}“ auswählen`}
                      className="exercise-card-layout__check"
                    />
                    <div className="exercise-card__body exercise-card-body-flex">
                      <h3 className="exercise-card-title">
                        <Link to={`/exercises/${exercise.id}`}>
                          {exercise.title}
                        </Link>
                      </h3>
                      <div className="exercise-card-tags">
                        {focusNames.map((name) => (
                          <span key={`fa:${name}`} className="exercise-tag exercise-tag--accent">{name}</span>
                        ))}
                        {styleNames.map((name) => (
                          <span key={`sd:${name}`} className="exercise-tag exercise-tag--style">{name}</span>
                        ))}
                        {typeNames.map((name) => (
                          <span key={`tt:${name}`} className="exercise-tag exercise-tag--training">{name}</span>
                        ))}
                      </div>
                      {exercise.summary && String(exercise.summary).trim() ? (
                        <div className="exercise-card-summary exercise-card-summary--rich">
                          <ExerciseRichTextBlock
                            html={exercise.summary}
                            exerciseId={exercise.id}
                            media={exercise.media || []}
                          />
                        </div>
                      ) : null}
                    </div>
                  </div>
                  <div className="exercise-card__footer">
                    <ExerciseCardScopeStatus exercise={exercise} />
                    <div className="exercise-card__actions exercise-card__actions--icons">
                    <Link
                      to={`/exercises/${exercise.id}`}
                      className="exercise-card__icon-btn"
                      title="Ansehen"
                      aria-label={`„${(exercise.title || 'Übung').replace(/"/g, '')}“ ansehen`}
                    >
                      <Eye size={18} strokeWidth={2} aria-hidden />
                    </Link>
                    <Link
                      to={`/exercises/${exercise.id}/edit`}
                      className="exercise-card__icon-btn"
                      title="Bearbeiten"
                      aria-label={`„${(exercise.title || 'Übung').replace(/"/g, '')}“ bearbeiten`}
                    >
                      <Pencil size={18} strokeWidth={2} aria-hidden />
                    </Link>
                    {canUserRequestExerciseDelete(user, exercise) ? (
                    <button
                      type="button"
                      className="exercise-card__icon-btn exercise-card__icon-btn--danger"
                      title="Löschen"
                      aria-label={`„${(exercise.title || 'Übung').replace(/"/g, '')}“ löschen`}
                      onClick={() => handleDelete(exercise)}
                    >
                      <Trash2 size={18} strokeWidth={2} aria-hidden />
                    </button>
                    ) : null}
                    </div>
                  </div>
                </div>
              )
            })}
          </div>
          {hasMore && (
            <div className="exercises-load-more">
              <button type="button" className="btn btn-secondary" disabled={loadingMore} onClick={loadMore}>
                {loadingMore ? 'Laden…' : 'Mehr laden'}
              </button>
            </div>
          )}
        </>
      )}
    </>
      )}
    </div>
  )
}

export default ExercisesListPage
