import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react'
import { useNavigate, useParams, Link, useLocation } from 'react-router-dom'
import api, { buildExerciseApiPayload } from '../../utils/api'
import { resolveExerciseMediaFileUrl, resolveMediaAssetFileUrl } from '../../utils/exerciseMediaUrl'
import RichTextEditor from '../RichTextEditor'
import ExerciseProgressionGraphPanel from '../ExerciseProgressionGraphPanel'
import ExerciseMediaThumbTile from '../ExerciseMediaThumbTile'
import MediaPreviewModal from '../MediaPreviewModal'
import ReportContentModal from '../ReportContentModal'
import CombinationMethodProfileEditor from '../CombinationMethodProfileEditor'
import ExercisePickerModal from '../ExercisePickerModal'
import {
  SHINKAN_EXERCISE_MEDIA_DRAG_MIME,
  buildExerciseMediaDragPayload,
} from '../../utils/exerciseInlineMediaRefs'
import { autoScrollForDragNearEdges } from '../../utils/dragAutoScroll'
import { normalizeSkillLevelSlug, formatSkillLevelSlug } from '../../constants/skillLevels'
import { stripHtmlToText } from '../../utils/htmlUtils'
import ExerciseCatalogAssocEditor from './ExerciseCatalogAssocEditor'
import ExerciseSkillsEditor from './ExerciseSkillsEditor'
import { useAuth } from '../../context/AuthContext'
import FeatureUsageBadge from '../FeatureUsageBadge'
import { useToast } from '../../context/ToastContext'
import {
  activeClubMemberships,
  getDefaultClubIdForGovernanceForms,
  getTenantClubDependencyKey,
} from '../../utils/activeClub'
import { COMBINATION_ARCHETYPE_OPTIONS, ARCHETYPE_DEFAULT_REP_SERIES_COUNT, defaultRepSeriesCountForArchetype } from '../../constants/combinationArchetypes'
import { readSlotProfilesV1, normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../../utils/combinationMethodProfileUi'
import { GripVertical, FileText, BookOpen, Tags, Layers, GitBranch, Image as ImageIcon } from 'lucide-react'
import UnsavedChangesPrompt from '../UnsavedChangesPrompt'
import PageFormEditorChrome from '../PageFormEditorChrome'
import { ExerciseFormTabBar, ExerciseFormPanel } from './ExerciseFormLayout'
import { useNavReturn } from '../../hooks/useNavReturn'
import {
  EXERCISES_LIST_PATH,
  buildCurrentLocationReturnContext,
  buildExercisesListReturnContext,
  linkStateWithAppReturn,
  preserveAppReturnOnNavigate,
} from '../../utils/navReturnContext'
import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../../hooks/useUnsavedChangesBlocker'

import {
  EXERCISE_SKILL_INTENSITY_DEFAULT,
  normalizeExerciseSkillIntensity,
  formatExerciseSkillIntensityLabel,
} from '../../constants/exerciseSkillIntensity'
import {
  EXERCISE_VISIBILITY_CLUB_FIELD_LABEL,
  EXERCISE_VISIBILITY_FIELD_LABEL,
} from '../../constants/exerciseGovernanceLabels'

const VARIANT_DIFFICULTY = [
  { value: '', label: '—' },
  { value: 'easier', label: 'Einfacher' },
  { value: 'same', label: 'Gleich' },
  { value: 'harder', label: 'Schwerer' },
  { value: 'adapted', label: 'Angepasst' },
]

/** HTML5-DnD für Kombi-Stationen (Reihenfolge = Ablauf). */
const DND_EXERCISE_COMBO_STATION = 'application/x-shinkan-exercise-combo-station-v1'

/** Pro Station meist 1 Übung; bis zu 3 wenn kurzer Auswahl-Pool sinnvoll ist. */
const MAX_COMBO_CANDIDATES_PER_STATION = 3

const comboTinyNumberInputSx = {
  width: '3.5rem',
  maxWidth: '100%',
  padding: '4px 6px',
  fontSize: '0.8125rem',
  textAlign: 'center',
}

function escapeHtmlText(s) {
  return String(s)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
}

/** Plaintext fuer RichTextEditor: ein bis mehrere Absaetze, ohne bestehendes HTML zu zerstoeren. */
function aiPlainSummaryToMinimalHtml(text) {
  const raw = String(text || '').trim()
  if (!raw) return ''
  const parts = raw.split(/\n+/).map((p) => p.trim()).filter(Boolean)
  const paras = parts.length ? parts : [raw]
  return paras.map((p) => `<p>${escapeHtmlText(p)}</p>`).join('')
}

const INSTRUCTION_AI_FIELD_DEFS = [
  { key: 'goal', label: 'Ziel' },
  { key: 'execution', label: 'Durchführung' },
  { key: 'preparation', label: 'Vorbereitung / Aufbau' },
  { key: 'trainer_notes', label: 'Hinweise für Trainer' },
]

function cloneExerciseSkillRows(rows) {
  return Array.isArray(rows) ? rows.map((s) => ({ ...s })) : []
}

function buildNormalizedAiSkillRowFromApi(sug) {
  const sid = Number(sug.skill_id)
  if (!Number.isFinite(sid)) return null
  return {
    skill_id: sid,
    intensity: normalizeExerciseSkillIntensity(sug.intensity),
    required_level: normalizeSkillLevelSlug(sug.required_level) || 'grundlagen',
    target_level:
      normalizeSkillLevelSlug(sug.target_level) ||
      normalizeSkillLevelSlug(sug.required_level) ||
      'grundlagen',
    is_primary: !!sug.is_primary,
    ai_suggested: true,
  }
}

function buildExerciseAiSuggestionPreview({
  mode,
  snapshotSummaryHtml,
  snapshotSkills,
  snapshotInstructions,
  apiRes,
}) {
  const summaryRequested = mode !== 'skills' && mode !== 'instructions'
  const skillsRequested = mode !== 'summary' && mode !== 'instructions'
  const instructionsRequested = mode === 'instructions'

  let summaryAfterHtml = null
  let summaryAfterPlain = ''
  if (summaryRequested && apiRes.summary?.text) {
    summaryAfterPlain = String(apiRes.summary.text).trim()
    if (summaryAfterPlain) {
      summaryAfterHtml = aiPlainSummaryToMinimalHtml(apiRes.summary.text)
    }
  }

  const skillChoices = []
  if (skillsRequested && Array.isArray(apiRes.skills)) {
    for (const sug of apiRes.skills) {
      const after = buildNormalizedAiSkillRowFromApi(sug)
      if (!after) continue
      const ix = snapshotSkills.findIndex((s) => Number(s.skill_id) === after.skill_id)
      const before = ix >= 0 ? { ...snapshotSkills[ix] } : null
      skillChoices.push({
        key: String(after.skill_id),
        skill_id: after.skill_id,
        kind: before ? 'update' : 'add',
        before,
        after,
        include: true,
      })
    }
  }

  const instructionChoices = []
  if (instructionsRequested && apiRes.instructions?.fields) {
    const fields = apiRes.instructions.fields
    const snap = snapshotInstructions || {}
    for (const def of INSTRUCTION_AI_FIELD_DEFS) {
      const afterHtml = fields[def.key]
      if (!afterHtml || !String(afterHtml).trim()) continue
      const beforeHtml = snap[def.key] || ''
      instructionChoices.push({
        key: def.key,
        field: def.key,
        label: def.label,
        beforePlain: stripHtmlToText(beforeHtml).trim(),
        afterHtml: String(afterHtml),
        afterPlain: stripHtmlToText(afterHtml).trim(),
        include: true,
      })
    }
  }

  const hasSummaryProposal = !!(summaryRequested && summaryAfterHtml)
  const hasSkillChoices = skillChoices.length > 0
  const hasInstructionChoices = instructionChoices.length > 0

  return {
    mode,
    applySummary: hasSummaryProposal,
    summaryBeforePlain: stripHtmlToText(snapshotSummaryHtml || '').trim(),
    summaryAfterPlain,
    summaryAfterHtml,
    skillChoices,
    instructionChoices,
    hasSummaryProposal,
    hasSkillChoices,
    hasInstructionChoices,
    summaryRequested,
    skillsRequested,
    instructionsRequested,
  }
}

function describeExerciseSkillRowForPreview(row, skillsCatalog) {
  if (!row) return ''
  const sk = skillsCatalog.find((x) => Number(x.id) === Number(row.skill_id))
  const name = sk?.name || `Fähigkeit #${row.skill_id}`
  const int = formatExerciseSkillIntensityLabel(row.intensity)
  const from = formatSkillLevelSlug(row.required_level) || '—'
  const to = formatSkillLevelSlug(row.target_level) || '—'
  const prim = row.is_primary ? ' · Primär' : ''
  return `${name}: Intensität ${int}, Niveau ${from} → ${to}${prim}`
}

function emptyComboSlotRow() {
  return {
    title: '',
    candidate_exercise_ids: [],
    exercise_title_by_id: {},
    advance_mode: 'timed',
    load_sec: '',
    consecutive_reps: '',
    rep_series_count: '1',
    intra_rep_rest_sec: '',
    transition_after_sec: '',
  }
}

function comboSlotsFromDetail(exercise) {
  const raw = exercise?.combination_slots
  const arch = exercise?.method_archetype != null ? String(exercise.method_archetype).trim() : ''
  const serienFallback = defaultRepSeriesCountForArchetype(arch)
  const mp =
    exercise?.method_profile &&
    typeof exercise.method_profile === 'object' &&
    !Array.isArray(exercise.method_profile)
      ? exercise.method_profile
      : {}
  const spvList = readSlotProfilesV1(mp)
  const byIx = new Map(spvList.map((r) => [Number(r.slot_index), r]))

  if (!Array.isArray(raw) || raw.length === 0) {
    return [emptyComboSlotRow()]
  }
  const sorted = [...raw].sort((a, b) => (Number(a.slot_index) || 0) - (Number(b.slot_index) || 0))
  return sorted.map((s) => {
    const si = Number(s.slot_index)
    const st = byIx.get(si) || {}
    const cands = Array.isArray(s.candidate_exercise_ids)
      ? s.candidate_exercise_ids.map((x) => Number(x)).filter((n) => Number.isFinite(n))
      : []
    const mode = normalizeAdvanceMode(st.advance_mode)
    let repSer = ''
    if (st.rep_series_count != null) repSer = String(st.rep_series_count)
    else if (mode === 'rep' || mode === 'manual') repSer = String(serienFallback)
    else repSer = '1'
    return {
      title: s.title != null ? String(s.title) : '',
      candidate_exercise_ids: cands,
      exercise_title_by_id: {},
      advance_mode: mode,
      load_sec: st.load_sec != null ? String(st.load_sec) : '',
      consecutive_reps: st.consecutive_reps != null ? String(st.consecutive_reps) : '',
      rep_series_count: repSer,
      intra_rep_rest_sec: st.intra_rep_rest_sec != null ? String(st.intra_rep_rest_sec) : '',
      transition_after_sec: st.transition_after_sec != null ? String(st.transition_after_sec) : '',
    }
  })
}

function emptyVariantDraft() {
  return {
    variant_name: '',
    description: '',
    execution_changes: '',
    duration_min: '',
    duration_max: '',
    equipment_lines: '',
    difficulty_adjustment: '',
    progression_level: 1,
    prerequisite_variant_id: '',
  }
}

function apiVariantToRow(v) {
  let lines = ''
  const eq = v.equipment_changes
  if (Array.isArray(eq)) {
    lines = eq.join('\n')
  } else if (typeof eq === 'string' && eq.trim()) {
    try {
      const p = JSON.parse(eq)
      lines = Array.isArray(p) ? p.join('\n') : eq
    } catch {
      lines = eq
    }
  }
  return {
    ...v,
    duration_min: v.duration_min ?? '',
    duration_max: v.duration_max ?? '',
    equipment_lines: lines,
    progression_level: v.progression_level ?? 1,
    prerequisite_variant_id: v.prerequisite_variant_id ?? '',
    difficulty_adjustment: v.difficulty_adjustment ?? '',
  }
}

function buildVariantPayloadFromRow(row) {
  const lines = (row.equipment_lines || '')
    .split(/[\n,]+/)
    .map((s) => s.trim())
    .filter(Boolean)
  const pl =
    row.progression_level === '' || row.progression_level == null
      ? 1
      : parseInt(row.progression_level, 10)
  const so =
    row.sequence_order === '' || row.sequence_order == null
      ? null
      : parseInt(row.sequence_order, 10)
  return {
    variant_name: (row.variant_name || '').trim(),
    description: (row.description || '').trim() || null,
    execution_changes: (row.execution_changes || '').trim() || null,
    duration_min: row.duration_min === '' || row.duration_min == null ? null : parseInt(row.duration_min, 10),
    duration_max: row.duration_max === '' || row.duration_max == null ? null : parseInt(row.duration_max, 10),
    equipment_changes: lines,
    difficulty_adjustment: row.difficulty_adjustment || null,
    progression_level: Number.isNaN(pl) ? 1 : pl,
    sequence_order: so !== null && Number.isNaN(so) ? null : so,
    prerequisite_variant_id:
      row.prerequisite_variant_id === '' || row.prerequisite_variant_id == null
        ? null
        : parseInt(row.prerequisite_variant_id, 10),
  }
}

function snapshotVariantPayload(row) {
  return JSON.stringify(buildVariantPayloadFromRow(row))
}

function variantDraftHasContent(draft) {
  if (!draft) return false
  const p = buildVariantPayloadFromRow(draft)
  return (
    p.variant_name.length > 0 ||
    Boolean(p.description) ||
    Boolean(p.execution_changes) ||
    p.duration_min != null ||
    p.duration_max != null ||
    (Array.isArray(p.equipment_changes) && p.equipment_changes.length > 0) ||
    Boolean(p.difficulty_adjustment) ||
    (p.progression_level != null && p.progression_level !== 1) ||
    p.prerequisite_variant_id != null
  )
}

/** Gemeinsame Felder für „Variante bearbeiten“ und „Neue Variante“. */
function ExerciseVariantFields({
  row,
  onPatch,
  prerequisiteOthers,
  rteMinHeight = '110px',
  inlineExerciseId,
  linkedExerciseMedia = [],
  onExerciseMediaListChanged,
}) {
  return (
    <>
      <div className="form-row">
        <label className="form-label">Variantenname *</label>
        <input
          type="text"
          className="form-input"
          value={row.variant_name || ''}
          onChange={(e) => onPatch({ variant_name: e.target.value })}
          minLength={3}
        />
      </div>
      <div className="form-row">
        <label className="form-label">Kurzbeschreibung</label>
        <textarea
          className="form-input"
          rows={2}
          value={row.description || ''}
          onChange={(e) => onPatch({ description: e.target.value })}
        />
      </div>
      <div className="form-row">
        <label className="form-label">Abweichungen zur Durchführung</label>
        <RichTextEditor
          value={row.execution_changes || ''}
          onChange={(html) => onPatch({ execution_changes: html })}
          placeholder="Was unterscheidet diese Variante? (Listen über Symbolleiste)"
          minHeight={rteMinHeight}
          inlineExerciseId={inlineExerciseId}
          linkedExerciseMedia={linkedExerciseMedia}
          onExerciseMediaListChanged={onExerciseMediaListChanged}
        />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
        <div className="form-row">
          <label className="form-label">Dauer Min</label>
          <input
            type="number"
            className="form-input"
            value={row.duration_min}
            onChange={(e) => onPatch({ duration_min: e.target.value })}
          />
        </div>
        <div className="form-row">
          <label className="form-label">Dauer Max</label>
          <input
            type="number"
            className="form-input"
            value={row.duration_max}
            onChange={(e) => onPatch({ duration_max: e.target.value })}
          />
        </div>
      </div>
      <div className="form-row">
        <label className="form-label">Materialänderungen (eine Zeile pro Eintrag)</label>
        <textarea
          className="form-input"
          rows={2}
          value={row.equipment_lines || ''}
          onChange={(e) => onPatch({ equipment_lines: e.target.value })}
          placeholder="+ Pratzen"
        />
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: '10px' }}>
        <div className="form-row">
          <label className="form-label">Schwere relativ</label>
          <select
            className="form-input"
            value={row.difficulty_adjustment || ''}
            onChange={(e) => onPatch({ difficulty_adjustment: e.target.value })}
          >
            {VARIANT_DIFFICULTY.map((o) => (
              <option key={o.label} value={o.value}>
                {o.label}
              </option>
            ))}
          </select>
        </div>
        <div className="form-row">
          <label className="form-label">Progressions-Stufe (1–10)</label>
          <input
            type="number"
            min={1}
            max={10}
            className="form-input"
            value={row.progression_level}
            onChange={(e) =>
              onPatch({
                progression_level: e.target.value === '' ? '' : parseInt(e.target.value, 10),
              })
            }
          />
        </div>
        <div className="form-row">
          <label className="form-label">Voraussetzungs-Variante</label>
          <select
            className="form-input"
            value={
              row.prerequisite_variant_id === '' || row.prerequisite_variant_id == null
                ? ''
                : String(row.prerequisite_variant_id)
            }
            onChange={(e) =>
              onPatch({
                prerequisite_variant_id: e.target.value === '' ? '' : parseInt(e.target.value, 10),
              })
            }
          >
            <option value="">— keine —</option>
            {prerequisiteOthers.map((o) => (
              <option key={o.id} value={o.id}>
                {o.variant_name || `Variante #${o.id}`}
              </option>
            ))}
          </select>
        </div>
      </div>
    </>
  )
}

function emptyForm() {
  return {
    title: '',
    summary: '',
    goal: '',
    execution: '',
    preparation: '',
    trainer_notes: '',
    equipmentLines: '',
    duration_min: '',
    duration_max: '',
    group_size_min: '',
    group_size_max: '',
    focus_areas_multi: [],
    training_styles_multi: [],
    training_types_multi: [],
    target_groups_multi: [],
    visibility: 'private',
    club_id: null,
    status: 'draft',
    skills: [],
    exercise_kind: 'simple',
    method_archetype: '',
    method_profile_json: '{}',
    combination_slots: [emptyComboSlotRow()],
  }
}

function detailToForm(exercise) {
  return {
    title: exercise.title || '',
    summary: exercise.summary || '',
    goal: exercise.goal || '',
    execution: exercise.execution || '',
    preparation: exercise.preparation || '',
    trainer_notes: exercise.trainer_notes || '',
    equipmentLines: (exercise.equipment || []).join('\n'),
    duration_min: exercise.duration_min ?? '',
    duration_max: exercise.duration_max ?? '',
    group_size_min: exercise.group_size_min ?? '',
    group_size_max: exercise.group_size_max ?? '',
    focus_areas_multi: (exercise.focus_areas || []).map((f) => ({
      focus_area_id: f.focus_area_id,
      is_primary: !!f.is_primary,
    })),
    training_styles_multi: (exercise.training_styles || []).map((t) => ({
      training_style_id: t.training_style_id,
      is_primary: !!t.is_primary,
    })),
    training_types_multi: (exercise.training_types || []).map((t) => ({
      training_type_id: t.training_type_id,
      is_primary: !!t.is_primary,
    })),
    target_groups_multi: (exercise.target_groups || []).map((g) => ({
      target_group_id: g.target_group_id,
      is_primary: !!g.is_primary,
    })),
    visibility: exercise.visibility || 'private',
    club_id:
      String(exercise.visibility || '').trim().toLowerCase() === 'club' &&
      exercise.club_id != null &&
      exercise.club_id !== ''
        ? Number(exercise.club_id)
        : null,
    status: exercise.status || 'draft',
    skills:
      exercise.skills?.map((s) => ({
        skill_id: s.skill_id,
        intensity: normalizeExerciseSkillIntensity(s.intensity),
        required_level: normalizeSkillLevelSlug(s.required_level),
        target_level: normalizeSkillLevelSlug(s.target_level),
        is_primary: !!s.is_primary,
        ai_suggested: !!s.ai_suggested,
      })) || [],
    exercise_kind:
      String(exercise.exercise_kind || 'simple').toLowerCase() === 'combination'
        ? 'combination'
        : 'simple',
    method_archetype: exercise.method_archetype != null ? String(exercise.method_archetype) : '',
    method_profile_json:
      typeof exercise.method_profile === 'object' &&
      exercise.method_profile != null &&
      !Array.isArray(exercise.method_profile)
        ? JSON.stringify(exercise.method_profile, null, 2)
        : '{}',
    combination_slots: comboSlotsFromDetail(exercise),
  }
}

function ExerciseFormPageRoot() {
  const { id: routeId } = useParams()
  const navigate = useNavigate()
  const location = useLocation()
  const exercisesListReturn = useMemo(() => buildExercisesListReturnContext(), [])
  const { goBack } = useNavReturn(exercisesListReturn)
  const { user } = useAuth()
  const isSuperadmin = user?.role === 'superadmin'
  const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
  const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])

  const [clubsForGovernanceForms, setClubsForGovernanceForms] = useState([])
  useEffect(() => {
    if (!isPlatformAdmin) {
      setClubsForGovernanceForms([])
      return undefined
    }
    let cancelled = false
    ;(async () => {
      try {
        const list = await api.listClubs()
        if (!cancelled) setClubsForGovernanceForms(Array.isArray(list) ? list : [])
      } catch {
        if (!cancelled) setClubsForGovernanceForms([])
      }
    })()
    return () => {
      cancelled = true
    }
  }, [isPlatformAdmin, tenantClubDepKey])

  const membershipClubRows = useMemo(() => activeClubMemberships(user?.clubs), [user?.clubs])

  /** Plattform-Admin: alle Vereine; sonst nur Mitgliedschafts-Vereine. */
  const visibilityClubChoices = useMemo(() => {
    if (isPlatformAdmin && clubsForGovernanceForms.length > 0) {
      return [...clubsForGovernanceForms].sort((a, b) =>
        String(a.name || '').localeCompare(String(b.name || ''), 'de'),
      )
    }
    return [...membershipClubRows].sort((a, b) =>
      String(a.name || '').localeCompare(String(b.name || ''), 'de'),
    )
  }, [isPlatformAdmin, clubsForGovernanceForms, membershipClubRows])

  const governanceDefaultClubId = useMemo(() => getDefaultClubIdForGovernanceForms(user), [user])

  const exerciseId = routeId && !Number.isNaN(parseInt(routeId, 10)) ? parseInt(routeId, 10) : null
  const isEdit = exerciseId != null

  const [formData, setFormData] = useState(emptyForm)
  const [skillsCatalog, setSkillsCatalog] = useState([])
  const [focusAreas, setFocusAreas] = useState([])
  const [styleDirections, setStyleDirections] = useState([])
  const [trainingTypes, setTrainingTypes] = useState([])
  const [targetGroups, setTargetGroups] = useState([])
  const [mediaList, setMediaList] = useState([])
  const [loading, setLoading] = useState(!!isEdit)
  const [saving, setSaving] = useState(false)
  const [formDirty, setFormDirty] = useState(false)
  const [skillPick, setSkillPick] = useState('')

  const toast = useToast()
  const allowUnloadBlock = Boolean(formDirty && !loading && !saving)
  useBeforeUnloadWhen(allowUnloadBlock)
  const blocker = useUnsavedChangesBlocker(allowUnloadBlock)
  const [variants, setVariants] = useState([])
  const [variantDraft, setVariantDraft] = useState(() => emptyVariantDraft())
  const [variantSavingId, setVariantSavingId] = useState(null)
  const [variantBusy, setVariantBusy] = useState(false)
  const [aiSuggestBusy, setAiSuggestBusy] = useState(false)
  const [aiSuggestionPreview, setAiSuggestionPreview] = useState(null)
  const [variantEditSelection, setVariantEditSelection] = useState(null)
  const [activeFormTab, setActiveFormTab] = useState('stammdaten')
  const variantsSavedSnapshotRef = useRef({})

  const exerciseFormTabs = useMemo(() => {
    const tabs = [
      { id: 'stammdaten', label: 'Stammdaten', icon: FileText },
      { id: 'anleitung', label: 'Anleitung', icon: BookOpen },
      { id: 'einordnung', label: 'Einordnung', icon: Tags },
    ]
    if (formData.exercise_kind === 'combination') {
      tabs.push({ id: 'kombination', label: 'Kombination', icon: Layers })
    }
    if (isEdit) {
      if (formData.exercise_kind !== 'combination') {
        tabs.push({
          id: 'varianten',
          label: variants.length > 0 ? `Varianten (${variants.length})` : 'Varianten',
          icon: GitBranch,
        })
      }
      tabs.push({ id: 'medien', label: 'Medien & Mehr', icon: ImageIcon })
    } else {
      tabs.push({ id: 'varianten', label: 'Varianten', icon: GitBranch, disabled: true })
      tabs.push({ id: 'medien', label: 'Medien & Mehr', icon: ImageIcon, disabled: true })
    }
    return tabs
  }, [formData.exercise_kind, isEdit, variants.length])

  useEffect(() => {
    const allowed = new Set(exerciseFormTabs.filter((t) => !t.disabled).map((t) => t.id))
    if (!allowed.has(activeFormTab)) setActiveFormTab('stammdaten')
  }, [exerciseFormTabs, activeFormTab])

  useEffect(() => {
    if (formData.exercise_kind === 'combination' && activeFormTab === 'varianten') {
      setActiveFormTab('kombination')
    }
  }, [formData.exercise_kind, activeFormTab])

  const syncVariantsSavedSnapshot = useCallback((rows) => {
    const snap = {}
    for (const v of rows || []) {
      if (v?.id != null) snap[v.id] = snapshotVariantPayload(v)
    }
    variantsSavedSnapshotRef.current = snap
  }, [])

  const getDirtyVariantRows = useCallback((rows) => {
    return (rows || []).filter((v) => {
      if (v?.id == null) return false
      const saved = variantsSavedSnapshotRef.current[v.id]
      if (saved == null) return true
      return snapshotVariantPayload(v) !== saved
    })
  }, [])

  const [mediaFields, setMediaFields] = useState({})
  const [mediaSavingId, setMediaSavingId] = useState(null)
  const [archiveOpen, setArchiveOpen] = useState(false)
  const [archiveQ, setArchiveQ] = useState('')
  const [archiveLoading, setArchiveLoading] = useState(false)
  const [archiveItems, setArchiveItems] = useState([])
  const [archiveError, setArchiveError] = useState(null)
  const [mediaPreview, setMediaPreview] = useState(null)
  const [reportTarget, setReportTarget] = useState(null)

  useEffect(() => {
    const next = {}
    for (const m of mediaList) {
      next[m.id] = {
        title: m.title || '',
      }
    }
    setMediaFields(next)
  }, [mediaList])

  useEffect(() => {
    const onDragOverDoc = (e) => {
      const types = e.dataTransfer?.types ? Array.from(e.dataTransfer.types) : []
      if (!types.includes(SHINKAN_EXERCISE_MEDIA_DRAG_MIME)) return
      e.preventDefault()
      autoScrollForDragNearEdges(e)
    }
    document.addEventListener('dragover', onDragOverDoc)
    return () => document.removeEventListener('dragover', onDragOverDoc)
  }, [])



  useEffect(() => {
    if (!archiveOpen) return undefined
    let cancelled = false
    const t = setTimeout(async () => {
      setArchiveLoading(true)
      setArchiveError(null)
      try {
        const res = await api.listMediaAssets({
          q: archiveQ.trim() || undefined,
          limit: 40,
        })
        if (!cancelled) setArchiveItems((res.items || []).filter(a => !a.legal_hold_active))
      } catch (e) {
        if (!cancelled) setArchiveError(e.message || String(e))
      } finally {
        if (!cancelled) setArchiveLoading(false)
      }
    }, 280)
    return () => {
      cancelled = true
      clearTimeout(t)
    }
  }, [archiveOpen, archiveQ])

  useEffect(() => {
    let cancelled = false
    const boot = async () => {
      try {
        const [skillsData, faData, sdData, ttData, tgData] = await Promise.all([
          api.listSkillsCatalog(),
          api.listFocusAreas(),
          api.listTrainingStyles(),
          api.listTrainingTypes(),
          api.listTargetGroups(),
        ])
        if (cancelled) return
        setSkillsCatalog(skillsData)
        setFocusAreas(faData)
        setStyleDirections(sdData)
        setTrainingTypes(ttData)
        setTargetGroups(tgData)
      } catch (e) {
        if (!cancelled) {
          console.error(e)
          toast.error(
            'Kataloge (Fokus, Stile, Zielgruppen, Fähigkeiten) konnten nicht geladen werden: ' +
              (e.message || e),
          )
        }
      }
    }
    boot()
    return () => {
      cancelled = true
    }
  }, [toast])

  useEffect(() => {
    if (!isEdit) {
      setFormData(emptyForm())
      setMediaList([])
      setVariants([])
      setVariantDraft(emptyVariantDraft())
      setVariantEditSelection(null)
      setFormDirty(false)
      setLoading(false)
      return
    }
    let cancelled = false
    const load = async () => {
      setLoading(true)
      try {
        const exercise = await api.getExercise(exerciseId)
        if (cancelled) return
        const variantRows = (exercise.variants || []).map(apiVariantToRow)
        setFormData(detailToForm(exercise))
        setMediaList(exercise.media || [])
        setVariants(variantRows)
        syncVariantsSavedSnapshot(variantRows)
        setVariantDraft(emptyVariantDraft())
        setVariantEditSelection(null)
        setFormDirty(false)
      } catch (err) {
        if (!cancelled) {
          toast.error(err.message || 'Übung nicht ladbar')
          navigate('/exercises')
        }
      } finally {
        if (!cancelled) setLoading(false)
      }
    }
    load()
    return () => {
      cancelled = true
    }
  }, [isEdit, exerciseId, navigate, toast])

  useEffect(() => {
    if (variantEditSelection == null || variantEditSelection === 'new') return
    if (!variants.some((v) => v.id === variantEditSelection)) {
      setVariantEditSelection(null)
    }
  }, [variants, variantEditSelection])

  useEffect(() => {
    if (variantEditSelection != null && isEdit && formData.exercise_kind !== 'combination') {
      setActiveFormTab('varianten')
    }
  }, [variantEditSelection, isEdit, formData.exercise_kind])

  const updateFormField = (field, value) => {
    setFormDirty(true)
    setFormData((prev) => ({ ...prev, [field]: value }))
  }

  useEffect(() => {
    if (formData.visibility !== 'club') return
    const choices = visibilityClubChoices
    if (!choices.length) return

    const id =
      formData.club_id != null && formData.club_id !== '' ? Number(formData.club_id) : NaN
    const hasValid =
      Number.isFinite(id) && id > 0 && choices.some((c) => Number(c.id) === id)

    if (hasValid) return

    const fallback = governanceDefaultClubId
    const next =
      fallback != null &&
      Number.isFinite(Number(fallback)) &&
      choices.some((c) => Number(c.id) === Number(fallback))
        ? Number(fallback)
        : Number(choices[0].id)

    setFormData((prev) => {
      if (prev.visibility !== 'club') return prev
      if (prev.club_id != null && Number(prev.club_id) === next) return prev
      return { ...prev, club_id: next }
    })
  }, [
    formData.visibility,
    formData.club_id,
    visibilityClubChoices,
    governanceDefaultClubId,
  ])

  const [comboStationPickerIx, setComboStationPickerIx] = useState(null)
  const [comboDropTargetIx, setComboDropTargetIx] = useState(null)

  const reorderCombinationSlots = (fromI, toBeforeIx) => {
    setFormDirty(true)
    setFormData((prev) => {
      const rows = [...(prev.combination_slots || [])]
      if (fromI < 0 || fromI >= rows.length) return prev
      const [moved] = rows.splice(fromI, 1)
      let insertAt = toBeforeIx
      if (fromI < toBeforeIx) insertAt = toBeforeIx - 1
      insertAt = Math.max(0, Math.min(insertAt, rows.length))
      rows.splice(insertAt, 0, moved)
      return { ...prev, combination_slots: rows }
    })
  }

  const patchComboSlotRow = (idx, patch) => {
    setFormDirty(true)
    setFormData((prev) => {
      const rows = [...(prev.combination_slots || [])]
      if (!rows[idx]) return prev
      rows[idx] = { ...rows[idx], ...patch }
      return { ...prev, combination_slots: rows }
    })
  }

  const removeCandidateFromSlot = (slotIdx, exerciseId) => {
    setFormDirty(true)
    setFormData((prev) => {
      const rows = [...(prev.combination_slots || [])]
      const row = rows[slotIdx]
      if (!row) return prev
      const nextIds = (row.candidate_exercise_ids || []).filter((id) => Number(id) !== Number(exerciseId))
      const labels = row.exercise_title_by_id && typeof row.exercise_title_by_id === 'object' ? { ...row.exercise_title_by_id } : {}
      delete labels[Number(exerciseId)]
      rows[slotIdx] = { ...row, candidate_exercise_ids: nextIds, exercise_title_by_id: labels }
      return { ...prev, combination_slots: rows }
    })
  }

  const mergePickedExercisesIntoSlot = (slotIdx, pickedList) => {
    if (!Array.isArray(pickedList) || !pickedList.length) return
    const rowNow = (formData.combination_slots || [])[slotIdx] || emptyComboSlotRow()
    const existingIds = Array.isArray(rowNow.candidate_exercise_ids)
      ? rowNow.candidate_exercise_ids.map((n) => Number(n)).filter((n) => Number.isFinite(n))
      : []
    const ordered = [...existingIds]
    pickedList.forEach((ex) => {
      if (ex?.id == null) return
      const id = Number(ex.id)
      if (!Number.isFinite(id)) return
      if (!ordered.includes(id)) ordered.push(id)
    })
    let nextIds = ordered
    if (nextIds.length > MAX_COMBO_CANDIDATES_PER_STATION) {
      toast.info(
        `Pro Station höchstens ${MAX_COMBO_CANDIDATES_PER_STATION} Übungen — üblich eine feste Übung; zwei bis drei nur als kleiner Wechsel‑Pool. Überschüssige Auswahl wurde abgeschnitten.`,
      )
      nextIds = nextIds.slice(0, MAX_COMBO_CANDIDATES_PER_STATION)
    }
    setFormDirty(true)
    setFormData((prev) => {
      const rows = [...(prev.combination_slots || [])]
      const row = rows[slotIdx] || emptyComboSlotRow()
      const labels =
        row.exercise_title_by_id && typeof row.exercise_title_by_id === 'object' ? { ...row.exercise_title_by_id } : {}
      pickedList.forEach((ex) => {
        if (ex && ex.id != null) {
          const id = Number(ex.id)
          const t = (ex.title || '').trim()
          if (t) labels[id] = t
        }
      })
      rows[slotIdx] = { ...row, candidate_exercise_ids: nextIds, exercise_title_by_id: labels }
      return { ...prev, combination_slots: rows }
    })
  }

  const addSkillRow = () => {
    const id = skillPick ? parseInt(skillPick, 10) : null
    if (!id) {
      toast.error('Fähigkeit wählen')
      return
    }
    if (formData.skills.some((s) => s.skill_id === id)) {
      toast.info('Bereits zugeordnet')
      return
    }
    updateFormField('skills', [
      ...formData.skills,
      {
        skill_id: id,
        intensity: EXERCISE_SKILL_INTENSITY_DEFAULT,
        required_level: '',
        target_level: '',
      },
    ])
    setSkillPick('')
  }

  const removeSkillRow = (idx) => {
    updateFormField(
      'skills',
      formData.skills.filter((_, i) => i !== idx),
    )
  }

  const updateSkillField = (idx, field, value) => {
    updateFormField(
      'skills',
      formData.skills.map((s, i) => (i === idx ? { ...s, [field]: value } : s)),
    )
  }

  const runExerciseAiSuggestion = async (mode) => {
    const gPlain = stripHtmlToText(formData.goal || '').trim()
    const ePlain = stripHtmlToText(formData.execution || '').trim()
    if (!gPlain && !ePlain) {
      toast.error('Ziel oder Durchführung ausfüllen — die KI benötigt Kontext.')
      return
    }

    const summaryOn = mode !== 'skills'
    const skillsOn = mode !== 'summary'

    const focusHint = (formData.focus_areas_multi || [])
      .map((row) => {
        const id = row?.focus_area_id
        const fa = focusAreas.find((x) => Number(x.id) === Number(id))
        return (fa?.name || '').trim()
      })
      .filter(Boolean)
      .join(', ')

    const snapshotSummaryHtml = formData.summary || ''
    const snapshotSkills = cloneExerciseSkillRows(formData.skills)

    const focusAreasContext = [...(formData.focus_areas_multi || [])]
      .map((row) => ({
        focus_area_id: Number(row?.focus_area_id),
        is_primary: !!row?.is_primary,
      }))
      .filter((x) => Number.isFinite(x.focus_area_id) && x.focus_area_id >= 1)
      .sort((a, b) => {
        const p = Number(!!b.is_primary) - Number(!!a.is_primary)
        if (p !== 0) return p
        return a.focus_area_id - b.focus_area_id
      })

    /* Vor jedem neuen Aufruf: Vorschau schließen; sonst bleiben die KI-Buttons wegen Modal-Zustand dauerhaft deaktiviert. */
    setAiSuggestionPreview(null)
    setAiSuggestBusy(true)
    try {
      const res = await api.suggestExerciseAi({
        title: (formData.title || '').trim(),
        goal: formData.goal || '',
        execution: formData.execution || '',
        focus_area_hint: focusHint || undefined,
        focus_areas_context: focusAreasContext.length ? focusAreasContext : undefined,
        include_summary: summaryOn,
        include_skills: skillsOn,
      })

      const preview = buildExerciseAiSuggestionPreview({
        mode,
        snapshotSummaryHtml,
        snapshotSkills,
        apiRes: res,
      })

      const hasSomething = preview.hasSummaryProposal || preview.hasSkillChoices
      if (!hasSomething) {
        toast.info('Die KI lieferte keinen verwertbaren Vorschlag für die gewählten Bereiche.')
        return
      }

      setAiSuggestionPreview(preview)
    } catch (err) {
      toast.error(err?.message || String(err))
    } finally {
      setAiSuggestBusy(false)
    }
  }

  const runExerciseAiInstructionRewrite = async () => {
    const title = (formData.title || '').trim()
    const snapshotInstructions = {
      goal: formData.goal || '',
      execution: formData.execution || '',
      preparation: formData.preparation || '',
      trainer_notes: formData.trainer_notes || '',
    }
    const hasSource =
      !!title ||
      Object.values(snapshotInstructions).some((html) => stripHtmlToText(html || '').trim())
    if (!hasSource) {
      toast.error('Titel oder mindestens ein Anleitungsfeld ausfüllen.')
      return
    }

    const focusHint = (formData.focus_areas_multi || [])
      .map((row) => {
        const id = row?.focus_area_id
        const fa = focusAreas.find((x) => Number(x.id) === Number(id))
        return (fa?.name || '').trim()
      })
      .filter(Boolean)
      .join(', ')

    const focusAreasContext = [...(formData.focus_areas_multi || [])]
      .map((row) => ({
        focus_area_id: Number(row?.focus_area_id),
        is_primary: !!row?.is_primary,
      }))
      .filter((x) => Number.isFinite(x.focus_area_id) && x.focus_area_id >= 1)
      .sort((a, b) => {
        const p = Number(!!b.is_primary) - Number(!!a.is_primary)
        if (p !== 0) return p
        return a.focus_area_id - b.focus_area_id
      })

    setAiSuggestionPreview(null)
    setAiSuggestBusy(true)
    try {
      const res = await api.suggestExerciseAi({
        title,
        goal: snapshotInstructions.goal,
        execution: snapshotInstructions.execution,
        preparation: snapshotInstructions.preparation,
        trainer_notes: snapshotInstructions.trainer_notes,
        focus_area_hint: focusHint || undefined,
        focus_areas_context: focusAreasContext.length ? focusAreasContext : undefined,
        include_summary: false,
        include_skills: false,
        include_instructions: true,
      })

      const preview = buildExerciseAiSuggestionPreview({
        mode: 'instructions',
        snapshotInstructions,
        apiRes: res,
      })

      if (!preview.hasInstructionChoices) {
        toast.info('Die KI lieferte keinen verwertbaren Anleitungs-Vorschlag.')
        return
      }

      setAiSuggestionPreview(preview)
    } catch (err) {
      toast.error(err?.message || String(err))
    } finally {
      setAiSuggestBusy(false)
    }
  }

  const applyExerciseAiSuggestionPreview = () => {
    const p = aiSuggestionPreview
    if (!p) return
    const takeSummary = !!(p.applySummary && p.summaryAfterHtml)
    const skillsToMerge = p.skillChoices.filter((c) => c.include).map((c) => c.after)
    const instrToApply = (p.instructionChoices || []).filter((c) => c.include && c.afterHtml)

    if (!takeSummary && skillsToMerge.length === 0 && instrToApply.length === 0) {
      toast.error('Bitte mindestens einen Vorschlag zur Übernahme auswählen.')
      return
    }

    if (takeSummary) {
      updateFormField('summary', p.summaryAfterHtml)
    }
    for (const c of instrToApply) {
      updateFormField(c.field, c.afterHtml)
    }
    if (skillsToMerge.length > 0) {
      setFormDirty(true)
      setFormData((prev) => {
        const next = [...(prev.skills || [])]
        for (const row of skillsToMerge) {
          const sid = Number(row.skill_id)
          const ix = next.findIndex((s) => Number(s.skill_id) === sid)
          if (ix >= 0) next[ix] = { ...next[ix], ...row }
          else next.push(row)
        }
        return { ...prev, skills: next }
      })
    }

    toast.success('Ausgewählte KI-Vorschläge übernommen — bitte prüfen und speichern.')
    setAiSuggestionPreview(null)
  }

  const discardExerciseAiSuggestionPreview = () => setAiSuggestionPreview(null)

  useEffect(() => {
    if (!aiSuggestionPreview) return undefined
    const onKey = (e) => {
      if (e.key === 'Escape') {
        e.preventDefault()
        setAiSuggestionPreview(null)
      }
    }
    window.addEventListener('keydown', onKey)
    return () => window.removeEventListener('keydown', onKey)
  }, [aiSuggestionPreview])

  const refreshVariants = useCallback(async () => {
    if (!exerciseId) return
    const ex = await api.getExercise(exerciseId)
    const rows = (ex.variants || []).map(apiVariantToRow)
    syncVariantsSavedSnapshot(rows)
    setVariants(rows)
  }, [exerciseId, syncVariantsSavedSnapshot])

  const createVariantFromDraft = useCallback(
    async ({ showSuccessToast = false } = {}) => {
      if (!exerciseId) return false
      if (!variantDraftHasContent(variantDraft)) return true
      const payload = buildVariantPayloadFromRow(variantDraft)
      if (payload.variant_name.length < 3) {
        toast.error('Variantenname mindestens 3 Zeichen')
        return false
      }
      setVariantBusy(true)
      try {
        const created = await api.createExerciseVariant(exerciseId, payload)
        setVariantDraft(emptyVariantDraft())
        if (created?.id != null) setVariantEditSelection(created.id)
        await refreshVariants()
        if (showSuccessToast) toast.success('Variante angelegt.')
        return true
      } catch (e) {
        toast.error(e.message || String(e))
        return false
      } finally {
        setVariantBusy(false)
      }
    },
    [exerciseId, variantDraft, refreshVariants, toast],
  )

  const persistPendingVariantChanges = useCallback(async () => {
    if (!exerciseId) return true

    const dirtyRows = getDirtyVariantRows(variants)
    if (dirtyRows.length > 0) {
      setVariantBusy(true)
      try {
        for (const row of dirtyRows) {
          const payload = buildVariantPayloadFromRow(row)
          if (payload.variant_name.length < 3) {
            toast.error(`Variante „${row.variant_name || `#${row.id}`}“: Name mindestens 3 Zeichen`)
            return false
          }
          setVariantSavingId(row.id)
          await api.updateExerciseVariant(exerciseId, row.id, payload)
        }
        await refreshVariants()
      } catch (e) {
        toast.error(e.message || String(e))
        return false
      } finally {
        setVariantSavingId(null)
        setVariantBusy(false)
      }
    }

    const draftOk = await createVariantFromDraft()
    return draftOk
  }, [exerciseId, variants, getDirtyVariantRows, refreshVariants, toast, createVariantFromDraft])

  const performSaveAttempt = useCallback(
    async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => {
      if (!formData.title || formData.title.trim().length < 3) {
        toast.error('Titel mindestens 3 Zeichen')
        return false
      }
      if (isEdit && exerciseId) {
        const variantsOk = await persistPendingVariantChanges()
        if (!variantsOk) return false
      }
      const payloadBase = {
        ...formData,
        equipment:
          typeof formData.equipmentLines === 'string'
            ? formData.equipmentLines
                .split(/[\n,]+/)
                .map((s) => s.trim())
                .filter(Boolean)
            : [],
      }
      let payload
      try {
        payload = buildExerciseApiPayload(payloadBase)
      } catch (err) {
        toast.error(err.message)
        return false
      }
      setSaving(true)
      try {
        if (isEdit) {
          const saveOnce = (extras = {}) =>
            api.updateExercise(exerciseId, buildExerciseApiPayload(payloadBase, extras))
          try {
            await saveOnce()
          } catch (firstErr) {
            if (
              firstErr.status === 422 &&
              firstErr.code === 'OFFICIAL_MEDIA_LIFECYCLE' &&
              firstErr.payload?.media_assets
            ) {
              toast.error(
                'Speichern nicht möglich: mindestens ein verknüpftes Medium ist nicht aktiv (Papierkorb). ' +
                  'Bitte Medium wiederherstellen oder aus der Übung entfernen.',
              )
              throw firstErr
            }
            if (firstErr.status === 422 && firstErr.code === 'OFFICIAL_MEDIA_CONFIRM_REQUIRED') {
              const promo = (firstErr.payload.assets_need_visibility_promotion || []).length
              const miss = (firstErr.payload.assets_missing_copyright || []).length
              let msg = 'Die Übung ist oder wird offiziell. '
              if (promo > 0) {
                msg += `${promo} verknüpfte Datei(en) werden dabei plattformweit („offiziell“) freigegeben. `
              }
              if (miss > 0) {
                msg += `${miss} Datei(en) haben noch keinen ausreichenden Copyright-Vermerk (mind. 3 Zeichen). `
              }
              msg += 'Fortfahren?'
              if (!window.confirm(msg)) throw firstErr
              let defaultCopyright = ''
              if (miss > 0) {
                defaultCopyright = window.prompt(
                  'Copyright-Vermerk für betroffene Dateien ohne Eintrag (mind. 3 Zeichen):',
                  '© ',
                )
                if (!defaultCopyright || String(defaultCopyright).trim().length < 3) {
                  toast.error('Mindestens 3 Zeichen für den Copyright-Vermerk.')
                  throw firstErr
                }
              }
              await saveOnce({
                promote_attached_media_for_official: true,
                ...(miss > 0 ? { default_official_media_copyright: String(defaultCopyright).trim() } : {}),
              })
            } else if (
              firstErr.status === 422 &&
              firstErr.code === 'CLUB_MEDIA_COPYRIGHT_REQUIRED' &&
              firstErr.payload?.media_assets
            ) {
              const miss = firstErr.payload.media_assets.length
              const msg =
                `Vereinsöffentliche Übungen brauchen bei jeder verknüpften Datei einen Copyright-Vermerk (mind. 3 Zeichen). ` +
                `${miss} Datei(en) sind noch ohne ausreichenden Vermerk. ` +
                `Beim Speichern einen gemeinsamen Vermerk für diese Dateien setzen?`
              if (!window.confirm(msg)) throw firstErr
              const defaultCopyright = window.prompt(
                'Copyright-Vermerk für die betroffenen Dateien (mind. 3 Zeichen):',
                '© ',
              )
              if (!defaultCopyright || String(defaultCopyright).trim().length < 3) {
                toast.error('Mindestens 3 Zeichen für den Copyright-Vermerk.')
                throw firstErr
              }
              await saveOnce({
                default_club_media_copyright: String(defaultCopyright).trim(),
              })
            } else if (firstErr.status === 422 && firstErr.code === 'CLUB_MEDIA_LIFECYCLE') {
              toast.error(
                'Speichern nicht möglich: mindestens ein verknüpftes Medium ist nicht aktiv (Papierkorb). Bitte reaktivieren oder entfernen.',
              )
              throw firstErr
            } else {
              throw firstErr
            }
          }
          const ex = await api.getExercise(exerciseId)
          setMediaList(ex.media || [])
          const variantRows = (ex.variants || []).map(apiVariantToRow)
          setVariants(variantRows)
          syncVariantsSavedSnapshot(variantRows)
          setFormDirty(false)
          toast.success('Gespeichert.')
          if (closeAfter) goBack()
          return true
        }
        const created = await api.createExercise(payload)
        setFormDirty(false)
        toast.success('Übung angelegt.')
        if (closeAfter) {
          goBack()
        } else if (!fromUnsavedDialog) {
          preserveAppReturnOnNavigate(navigate, location, `/exercises/${created.id}/edit`, { replace: true })
        }
        return true
      } catch (err) {
        toast.error('Fehler beim Speichern: ' + err.message)
        return false
      } finally {
        setSaving(false)
      }
    },
    [exerciseId, formData, isEdit, navigate, location, toast, goBack, persistPendingVariantChanges, syncVariantsSavedSnapshot],
  )

  const handleSubmit = useCallback(
    async (e) => {
      e?.preventDefault?.()
      await performSaveAttempt({ fromUnsavedDialog: false, closeAfter: false })
    },
    [performSaveAttempt],
  )

  const handleSaveAndClose = useCallback(
    async (e) => {
      e?.preventDefault?.()
      await performSaveAttempt({ fromUnsavedDialog: false, closeAfter: true })
    },
    [performSaveAttempt],
  )

  const actionConfig = useMemo(
    () => ({
      formId: 'exercise-form',
      saving,
      isNew: !isEdit,
      onSave: handleSubmit,
      onSaveAndClose: handleSaveAndClose,
      onCancel: goBack,
      showSave: true,
      showSaveAndClose: true,
    }),
    [saving, isEdit, handleSubmit, handleSaveAndClose, goBack],
  )

  const handleUnsavedDialogSave = async () => {
    const ok = await performSaveAttempt({ fromUnsavedDialog: true })
    if (ok) blocker.proceed()
  }

  const refreshMedia = async () => {
    if (!exerciseId) return
    const ex = await api.getExercise(exerciseId)
    setMediaList(ex.media || [])
  }

  const attachFromArchive = async (assetId) => {
    if (!exerciseId) return
    try {
      await api.attachExerciseMediaFromAsset(exerciseId, {
        media_asset_id: assetId,
        context: 'ablauf',
        title: '',
        description: '',
        is_primary: false,
      })
      setArchiveOpen(false)
      await refreshMedia()
    } catch (e) {
      toast.error(e.message || String(e))
    }
  }

  const linkedArchiveAssetIds = useMemo(
    () => new Set((mediaList || []).map((m) => m.media_asset_id).filter(Boolean)),
    [mediaList],
  )

  const handleDeleteMedia = async (mid) => {
    if (
      !confirm(
        'Dieses Medium aus der Übung entfernen? Nur die Verknüpfung wird gelöscht. Die Datei bleibt im Archiv, solange sie noch woanders genutzt wird.\n\n' +
          'Hinweis: Wenn dieser Eintrag noch als Platzhalter im Fließtext steht, zeigt die Vorschau [Medium nicht verfügbar] oder das Speichern der Übung schlägt fehl, bis der Platzhalter entfernt ist.',
      )
    ) {
      return
    }
    try {
      const res = await api.deleteExerciseMedia(exerciseId, mid)
      await refreshMedia()
      const oid = res?.orphan_media_asset_id
      if (oid != null) {
        if (
          confirm(
            'Dieses Archiv-Medium wird danach nirgendwo mehr verwendet. In den Papierkorb (Stufe 1) legen? (Später in der Medienverwaltung wiederherstellbar.)',
          )
        ) {
          await api.postMediaAssetLifecycle(oid, 'trash_soft')
          await refreshMedia()
        }
      }
    } catch (err) {
      toast.error(err.message)
    }
  }

  const moveMediaRow = async (idx, dir) => {
    if (!exerciseId) return
    const j = idx + dir
    if (j < 0 || j >= mediaList.length) return
    const next = [...mediaList]
    const tmp = next[idx]
    next[idx] = next[j]
    next[j] = tmp
    try {
      await api.reorderExerciseMedia(
        exerciseId,
        next.map((x) => x.id),
      )
      setMediaList(next)
    } catch (e) {
      toast.error(e.message || String(e))
    }
  }

  const saveMediaMeta = async (mid) => {
    if (!exerciseId) return
    const fld = mediaFields[mid]
    if (!fld) return
    setMediaSavingId(mid)
    try {
      await api.updateExerciseMedia(exerciseId, mid, {
        title: fld.title.trim() || null,
      })
      await refreshMedia()
    } catch (e) {
      toast.error(e.message || String(e))
    } finally {
      setMediaSavingId(null)
    }
  }

  const updateVariantField = (id, patch) => {
    setFormDirty(true)
    setVariants((prev) => prev.map((v) => (v.id === id ? { ...v, ...patch } : v)))
  }

  const saveVariantRow = async (row) => {
    const payload = buildVariantPayloadFromRow(row)
    if (payload.variant_name.length < 3) {
      toast.error('Variantenname mindestens 3 Zeichen')
      return
    }
    setVariantSavingId(row.id)
    try {
      await api.updateExerciseVariant(exerciseId, row.id, payload)
      await refreshVariants()
    } catch (e) {
      toast.error(e.message || String(e))
    } finally {
      setVariantSavingId(null)
    }
  }

  const deleteVariantRow = async (id) => {
    if (!confirm('Variante wirklich löschen?')) return
    setVariantBusy(true)
    try {
      await api.deleteExerciseVariant(exerciseId, id)
      if (variantEditSelection === id) setVariantEditSelection(null)
      await refreshVariants()
    } catch (e) {
      toast.error(e.message || String(e))
    } finally {
      setVariantBusy(false)
    }
  }

  const moveVariantRow = async (idx, dir) => {
    const j = idx + dir
    if (j < 0 || j >= variants.length) return
    const next = [...variants]
    const tmp = next[idx]
    next[idx] = next[j]
    next[j] = tmp
    const ids = next.map((x) => x.id)
    setVariantBusy(true)
    try {
      await api.reorderExerciseVariants(exerciseId, ids)
      await refreshVariants()
    } catch (e) {
      toast.error(e.message || String(e))
    } finally {
      setVariantBusy(false)
    }
  }

  const handleCreateVariantClick = useCallback(async () => {
    await createVariantFromDraft({ showSuccessToast: true })
  }, [createVariantFromDraft])

  const selectedVariantForEdit =
    typeof variantEditSelection === 'number' ? variants.find((v) => v.id === variantEditSelection) : null
  const selectedVariantIdx = selectedVariantForEdit
    ? variants.findIndex((v) => v.id === selectedVariantForEdit.id)
    : -1

  if (loading) {
    return (
      <div style={{ padding: '2rem', textAlign: 'center' }}>
        <div className="spinner"></div>
        <p>Laden...</p>
      </div>
    )
  }

  return (
    <PageFormEditorChrome
      testId="exercise-form-page"
      title={isEdit ? 'Übung bearbeiten' : 'Neue Übung'}
      fallbackPath={EXERCISES_LIST_PATH}
      fallbackLabel="Zurück zur Übungsliste"
      actionConfig={actionConfig}
    >
      {isEdit ? (
        <p style={{ margin: '0 0 12px' }}>
          <Link
            to={`/exercises/${exerciseId}`}
            state={linkStateWithAppReturn(
              buildCurrentLocationReturnContext(location, 'Zurück zur Bearbeitung')
            )}
            className="btn btn-secondary"
            style={{ fontSize: '0.875rem' }}
          >
            Ansehen
          </Link>
        </p>
      ) : null}

      <div className="card exercise-form-edit">
        <form id="exercise-form" onSubmit={handleSubmit}>
          <ExerciseFormTabBar
            activeTab={activeFormTab}
            onChange={setActiveFormTab}
            items={exerciseFormTabs}
          />

          <ExerciseFormPanel
            tab="stammdaten"
            activeTab={activeFormTab}
            tone="basics"
            title="Stammdaten"
            hint={
              isEdit
                ? 'Titel, Rahmendaten und Freigabelevel — Inhalt und Einordnung in den anderen Tabs.'
                : 'Titel und Rahmendaten. Varianten, Medien und Progressionsgraph sind nach dem ersten Speichern verfügbar.'
            }
          >
          <div className="form-row">
            <label className="form-label">Titel *</label>
            <input
              type="text"
              className="form-input"
              value={formData.title}
              onChange={(e) => updateFormField('title', e.target.value)}
              required
              minLength={3}
            />
          </div>

          <div className="form-row">
            <div
              style={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'space-between',
                gap: '8px',
                flexWrap: 'wrap',
              }}
            >
              <label className="form-label" style={{ marginBottom: 0 }}>
                Kurzbeschreibung
              </label>
              <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
                <FeatureUsageBadge featureId="ai_calls" />
                <button
                  type="button"
                  className="btn btn-secondary"
                  style={{ fontSize: '12px' }}
                  disabled={aiSuggestBusy}
                  onClick={() => runExerciseAiSuggestion('summary')}
                >
                  KI: Kurzfassung
                </button>
              </div>
            </div>
            <RichTextEditor
              value={formData.summary}
              onChange={(html) => updateFormField('summary', html)}
              placeholder="Kurzbeschreibung (optional)"
              minHeight="80px"
              inlineExerciseId={isEdit ? exerciseId : null}
              linkedExerciseMedia={isEdit ? mediaList : []}
              onExerciseMediaListChanged={refreshMedia}
            />
          </div>

          <div className="exercise-form-type-box">
            <div className="form-row">
              <label className="form-label">Art</label>
              <select
                className="form-input"
                value={formData.exercise_kind === 'combination' ? 'combination' : 'simple'}
                onChange={(e) => {
                  const nk = e.target.value
                  setFormDirty(true)
                  setFormData((prev) => ({
                    ...prev,
                    exercise_kind: nk,
                    ...(nk === 'simple'
                      ? {
                          method_archetype: '',
                          method_profile_json: '{}',
                          combination_slots: [emptyComboSlotRow()],
                        }
                      : {}),
                  }))
                  if (nk === 'combination') setActiveFormTab('kombination')
                }}
              >
                <option value="simple">Einzelübung</option>
                <option value="combination">Kombinationsübung (Stationen / Pool)</option>
              </select>
            </div>
            {formData.exercise_kind === 'combination' ? (
              <p className="exercise-form-type-box__hint">
                Stationen und Ablaufprofil im Tab{' '}
                <button type="button" className="exercise-form-inline-tab-link" onClick={() => setActiveFormTab('kombination')}>
                  Kombination
                </button>
                .
              </p>
            ) : null}
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
            <div className="form-row">
              <label className="form-label">Dauer Min</label>
              <input
                type="number"
                className="form-input"
                value={formData.duration_min}
                onChange={(e) =>
                  updateFormField('duration_min', e.target.value ? parseInt(e.target.value, 10) : '')
                }
              />
            </div>
            <div className="form-row">
              <label className="form-label">Dauer Max</label>
              <input
                type="number"
                className="form-input"
                value={formData.duration_max}
                onChange={(e) =>
                  updateFormField('duration_max', e.target.value ? parseInt(e.target.value, 10) : '')
                }
              />
            </div>
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
            <div className="form-row">
              <label className="form-label">Gruppe Min</label>
              <input
                type="number"
                className="form-input"
                value={formData.group_size_min}
                onChange={(e) =>
                  updateFormField('group_size_min', e.target.value ? parseInt(e.target.value, 10) : '')
                }
              />
            </div>
            <div className="form-row">
              <label className="form-label">Gruppe Max</label>
              <input
                type="number"
                className="form-input"
                value={formData.group_size_max}
                onChange={(e) =>
                  updateFormField('group_size_max', e.target.value ? parseInt(e.target.value, 10) : '')
                }
              />
            </div>
          </div>

          <div className="form-row">
            <label className="form-label">Material (eine Zeile oder kommagetrennt)</label>
            <textarea
              className="form-input"
              rows={3}
              value={formData.equipmentLines}
              onChange={(e) => updateFormField('equipmentLines', e.target.value)}
              placeholder="Matten&#10;Pratzen"
            />
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' }}>
            <div className="form-row">
              <label className="form-label">{EXERCISE_VISIBILITY_FIELD_LABEL}</label>
              <select
                className="form-input"
                value={formData.visibility}
                onChange={(e) => updateFormField('visibility', e.target.value)}
              >
                <option value="private">Privat</option>
                <option value="club">Verein</option>
                {isSuperadmin ? <option value="official">Offiziell</option> : null}
              </select>
            </div>
            <div className="form-row">
              <label className="form-label">Status</label>
              <select
                className="form-input"
                value={formData.status}
                onChange={(e) => updateFormField('status', e.target.value)}
              >
                <option value="draft">Entwurf</option>
                <option value="in_review">In Prüfung</option>
                <option value="approved">Freigegeben</option>
                <option value="archived">Archiviert</option>
              </select>
            </div>
          </div>

          {formData.visibility === 'club' && visibilityClubChoices.length > 0 ? (
            <div className="form-row" style={{ marginTop: '10px' }}>
              <label className="form-label">{EXERCISE_VISIBILITY_CLUB_FIELD_LABEL}</label>
              <select
                className="form-input"
                value={formData.club_id != null && formData.club_id !== '' ? String(formData.club_id) : ''}
                onChange={(e) => {
                  const v = e.target.value
                  updateFormField('club_id', v === '' ? null : Number(v))
                }}
              >
                {visibilityClubChoices.map((c) => (
                  <option key={c.id} value={String(c.id)}>
                    {(c.name || '').trim() || `Verein #${c.id}`}
                  </option>
                ))}
              </select>
              <p style={{ margin: '6px 0 0', fontSize: '12px', color: 'var(--text3)', lineHeight: 1.4 }}>
                Standard ist der aktive Verein aus der Navigation. Bei Plattform-Admins sind alle Vereine wählbar.
              </p>
            </div>
          ) : null}
          </ExerciseFormPanel>

          <ExerciseFormPanel
            tab="kombination"
            activeTab={activeFormTab}
            tone="combo"
            title="Kombinationsübung"
            hint="Stationen, Übungs-Pools und globales Ablaufprofil für Coach und Planung."
          >
            {formData.exercise_kind === 'combination' ? (
              <>
                <div className="form-row">
                  <label className="form-label">Methoden-Archetyp (für Coach &amp; Planung empfohlen)</label>
                  <select
                    className="form-input"
                    value={formData.method_archetype || ''}
                    onChange={(e) => {
                      const arch = (e.target.value || '').trim()
                      const forced = ARCHETYPE_DEFAULT_REP_SERIES_COUNT[arch]
                      setFormDirty(true)
                      setFormData((prev) => {
                        const slots = prev.combination_slots || []
                        const nextSlots =
                          forced !== undefined && forced !== null
                            ? slots.map((row) =>
                                normalizeAdvanceMode(row.advance_mode) !== 'timed'
                                  ? {
                                      ...row,
                                      rep_series_count: String(Math.max(1, Math.round(Number(forced)))),
                                    }
                                  : row,
                              )
                            : slots
                        return { ...prev, method_archetype: arch, combination_slots: nextSlots }
                      })
                    }}
                  >
                    <option value="">— noch nicht festgelegt —</option>
                    {COMBINATION_ARCHETYPE_OPTIONS.map((o) => (
                      <option key={o.id} value={o.id}>
                        {o.label}
                      </option>
                    ))}
                  </select>
                </div>
                {String(formData.method_archetype || '').trim() === 'station_parcour' ? (
                  <p
                    style={{
                      fontSize: '12px',
                      color: 'var(--text2)',
                      margin: '4px 0 10px',
                      lineHeight: 1.48,
                      padding: '10px 12px',
                      borderRadius: '8px',
                      background: 'var(--surface)',
                      border: '1px solid var(--border)',
                    }}
                  >
                    <strong>Parcours / Bahnsystem:</strong> typischerweise starten alle an Station 1 und durchlaufen der
                    Reihe nach alle Punkte (Geschwindigkeit variabel). Die Stationsreihenfolge unten ist der Ablaufweg;
                    Zeitangaben pro Station und <strong>Gesamtdurchläufe</strong> im Ablaufprofil strukturieren das
                    spätere Coaching.
                  </p>
                ) : null}
                <div style={{ paddingTop: '4px', borderTop: '1px dashed var(--border)', marginBottom: '12px' }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: '8px', flexWrap: 'wrap' }}>
                    <strong style={{ fontSize: '14px' }}>Stationen</strong>
                    <span style={{ fontSize: '11px', color: 'var(--text3)' }}>
                      Ablauf = Reihenfolge · ziehen / Pfeile · Einzelübungen · max. {MAX_COMBO_CANDIDATES_PER_STATION}/Station
                    </span>
                  </div>
                  <p style={{ fontSize: '12px', color: 'var(--text2)', margin: '6px 0 12px', lineHeight: 1.48 }}>
                    Pro Station oft <strong>eine</strong> feste Übung; höchstens <strong>drei</strong> als kleiner Auswahl‑Pool.
                    Unter <strong>Steuerung</strong> wählen: zeitlich, nach Wiederholungszahl oder ohne Arbeitsuhr (Coach führt).
                  </p>
                  {(formData.combination_slots || []).map((row, idx) => {
                    const candIds = Array.isArray(row.candidate_exercise_ids) ? row.candidate_exercise_ids : []
                    const comboPoolFull = candIds.length >= MAX_COMBO_CANDIDATES_PER_STATION
                    const slotAdv = normalizeAdvanceMode(row.advance_mode)
                    const serieLabel =
                      slotAdv === 'timed' ? 'Serie' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert'
                    const seriePlaceholder = slotAdv === 'rep' ? '10' : slotAdv === 'manual' ? '–' : '1'
                    const showMultiSeries = slotAdv === 'rep' || slotAdv === 'manual'
                    const serienCountUi = parseComboRepSeriesCountUi(row.rep_series_count)
                    const showInterSeriesPause = showMultiSeries && serienCountUi >= 2
                    const intraLabel = slotAdv === 'timed' ? 'Pause (s)' : 'Pause zw. Serien'
                    const lbl =
                      row.exercise_title_by_id && typeof row.exercise_title_by_id === 'object'
                        ? row.exercise_title_by_id
                        : {}
                    const isDropHere = comboDropTargetIx === idx
                    return (
                      <div
                        key={`combo-slot-${idx}`}
                        onDragOver={(e) => {
                          if (!e.dataTransfer?.types?.includes?.(DND_EXERCISE_COMBO_STATION)) return
                          e.preventDefault()
                          e.dataTransfer.dropEffect = 'move'
                          setComboDropTargetIx(idx)
                        }}
                        onDragLeave={() => setComboDropTargetIx((cur) => (cur === idx ? null : cur))}
                        onDrop={(e) => {
                          const rawFrom = e.dataTransfer.getData(DND_EXERCISE_COMBO_STATION)
                          const fromI = parseInt(rawFrom, 10)
                          e.preventDefault()
                          setComboDropTargetIx(null)
                          if (!Number.isFinite(fromI)) return
                          reorderCombinationSlots(fromI, idx)
                        }}
                        style={{
                          marginBottom: '12px',
                          padding: '12px 14px',
                          borderRadius: '12px',
                          border: `1px solid ${isDropHere ? 'var(--accent)' : 'var(--border)'}`,
                          background: 'var(--surface)',
                          boxShadow: isDropHere ? '0 0 0 2px var(--accent-soft)' : 'none',
                        }}
                      >
                        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'flex-start', marginBottom: '12px' }}>
                          <button
                            type="button"
                            draggable
                            onDragStart={(e) => {
                              e.dataTransfer.effectAllowed = 'move'
                              e.dataTransfer.setData(DND_EXERCISE_COMBO_STATION, String(idx))
                            }}
                            onDragEnd={() => setComboDropTargetIx(null)}
                            aria-label={`Station ${idx + 1} ziehen`}
                            title="Ziehen zum Sortieren"
                            className="btn btn-secondary framework-ctrl framework-ctrl--xs"
                            style={{ cursor: 'grab', padding: '6px 8px', touchAction: 'none' }}
                          >
                            <GripVertical size={18} strokeWidth={2} aria-hidden />
                          </button>
                          <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
                            <button
                              type="button"
                              className="btn btn-secondary framework-ctrl framework-ctrl--xs"
                              aria-label="Station nach oben"
                              disabled={idx === 0}
                              onClick={() => reorderCombinationSlots(idx, idx - 1)}
                            >
                              ▲
                            </button>
                            <button
                              type="button"
                              className="btn btn-secondary framework-ctrl framework-ctrl--xs"
                              aria-label="Station nach unten"
                              disabled={idx === (formData.combination_slots || []).length - 1}
                              onClick={() => reorderCombinationSlots(idx, idx + 2)}
                            >
                              ▼
                            </button>
                          </div>
                          <div className="form-row" style={{ flex: '1 1 200px', marginBottom: 0 }}>
                            <label className="form-label" style={{ fontSize: '12px' }}>
                              Name (St.&nbsp;{idx + 1})
                            </label>
                            <input
                              type="text"
                              className="form-input"
                              value={row.title || ''}
                              placeholder="z.&nbsp;B. Liegestütz"
                              onChange={(e) => patchComboSlotRow(idx, { title: e.target.value })}
                            />
                          </div>
                          <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'flex-end' }}>
                            <button
                              type="button"
                              className="btn btn-secondary framework-ctrl framework-ctrl--xs"
                              disabled={comboPoolFull}
                              title={
                                comboPoolFull
                                  ? `Max. ${MAX_COMBO_CANDIDATES_PER_STATION} Übungen — eine entfernen, um weitere zu wählen.`
                                  : 'Einzelübung zur Station hinzufügen'
                              }
                              onClick={() => setComboStationPickerIx(idx)}
                            >
                              + Übung
                            </button>
                            <button
                              type="button"
                              className="btn framework-ctrl framework-ctrl--xs"
                              style={{ fontSize: '12px' }}
                              title="Diese Station entfernen"
                              onClick={() => {
                                const prev = formData.combination_slots || []
                                const next = prev.filter((_, j) => j !== idx)
                                updateFormField('combination_slots', next.length ? next : [emptyComboSlotRow()])
                              }}
                            >
                              Entfernen
                            </button>
                          </div>
                        </div>
                        <div style={{ marginBottom: '12px' }}>
                          <span className="form-label" style={{ fontSize: '11px', display: 'block', marginBottom: '6px' }}>
                            Übungen ({candIds.length}/{MAX_COMBO_CANDIDATES_PER_STATION})
                          </span>
                          {candIds.length === 0 ? (
                            <p style={{ fontSize: '12px', color: 'var(--text3)', margin: 0 }}>
                              Mindestens eine Übung — mit „+ Übung“ wählen.
                            </p>
                          ) : (
                            <ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
                              {candIds.map((id) => (
                                <li
                                  key={`${idx}-c-${id}`}
                                  style={{
                                    display: 'inline-flex',
                                    alignItems: 'center',
                                    gap: '6px',
                                    padding: '4px 10px',
                                    borderRadius: '999px',
                                    border: '1px solid var(--border)',
                                    background: 'var(--surface2)',
                                    fontSize: '12px',
                                  }}
                                >
                                  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: '14rem' }} title={`#${id}`}>
                                    {(lbl[id] || lbl[String(id)] || '').trim() || `Übung #${id}`}
                                  </span>
                                  <button
                                    type="button"
                                    className="tu-icon-btn"
                                    aria-label={`Übung ${id} entfernen`}
                                    title="Entfernen"
                                    onClick={() => removeCandidateFromSlot(idx, id)}
                                  >
                                    ✗
                                  </button>
                                </li>
                              ))}
                            </ul>
                          )}
                        </div>
                        <div className="form-row" style={{ marginBottom: '8px', maxWidth: '22rem' }}>
                          <label className="form-label" style={{ fontSize: '11px' }}>
                            Steuerung
                          </label>
                          <select
                            className="form-input"
                            style={{ fontSize: '0.8125rem' }}
                            value={slotAdv}
                            onChange={(e) => {
                              const m = normalizeAdvanceMode(e.target.value)
                              const patch = { advance_mode: m }
                              if (m !== 'timed') patch.load_sec = ''
                              if (m === 'rep' || m === 'manual') {
                                const curSer = String(row.rep_series_count ?? '').trim()
                                if (!curSer) {
                                  patch.rep_series_count = String(
                                    defaultRepSeriesCountForArchetype(formData.method_archetype || ''),
                                  )
                                }
                              }
                              patchComboSlotRow(idx, patch)
                            }}
                          >
                            <option value="timed">Zeit (Arbeit in Sekunden)</option>
                            <option value="rep">Wiederholungen (Ziel)</option>
                            <option value="manual">Coach (Weiter nach Freigabe)</option>
                          </select>
                        </div>
                        <p style={{ fontSize: '11px', color: 'var(--text3)', margin: '0 0 10px', lineHeight: 1.42 }}>
                          {slotAdv === 'timed'
                            ? 'Arbeit (s): geplantes Ende nach Countdown möglich. Serie: Wiederholungen ohne Stationswechsel innerhalb einer Phase.'
                            : slotAdv === 'rep'
                              ? 'Ohne Pflicht-Arbeits-Timer: Ziel über Wiederholungen. Ab zwei Serien: Pause zwischen diesen Serien; sonst nur Wechsel zur nächsten Station.'
                              : 'Coach: keine feste Arbeitsuhr — Fortschritt später per Tipp. Ab 2 Serien: Pause zwischen Serien; sonst nur Wechsel zur nächsten Station zeitlich planen.'}
                        </p>
                        <div
                          style={{
                            display: 'grid',
                            gridTemplateColumns: 'repeat(auto-fill, minmax(5.25rem, 1fr))',
                            gap: '8px 10px',
                            alignItems: 'end',
                          }}
                        >
                          {slotAdv === 'timed' ? (
                            <div className="form-row" style={{ marginBottom: 0 }}>
                              <label className="form-label" style={{ fontSize: '10px' }}>
                                Arbeit (s)
                              </label>
                              <input
                                type="number"
                                min={0}
                                className="form-input"
                                style={comboTinyNumberInputSx}
                                placeholder="–"
                                value={row.load_sec || ''}
                                onChange={(e) => patchComboSlotRow(idx, { load_sec: e.target.value })}
                              />
                            </div>
                          ) : null}
                          <div className="form-row" style={{ marginBottom: 0 }}>
                            <label className="form-label" style={{ fontSize: '10px' }}>
                              {serieLabel}
                            </label>
                            <input
                              type="number"
                              min={slotAdv === 'rep' ? 1 : undefined}
                              className="form-input"
                              style={comboTinyNumberInputSx}
                              placeholder={seriePlaceholder}
                              value={row.consecutive_reps || ''}
                              onChange={(e) => patchComboSlotRow(idx, { consecutive_reps: e.target.value })}
                            />
                          </div>
                          {showMultiSeries ? (
                            <div className="form-row" style={{ marginBottom: 0 }}>
                              <label className="form-label" style={{ fontSize: '10px' }} title="Wie oft die angegebene Wdh.-Zahl hintereinander (mit Pause zw. Serien)?">
                                Serien
                              </label>
                              <input
                                type="number"
                                min={slotAdv === 'rep' ? 1 : undefined}
                                className="form-input"
                                style={comboTinyNumberInputSx}
                                placeholder="1"
                                value={row.rep_series_count || ''}
                                onChange={(e) => {
                                  let rawSer = e.target.value.trim()
                                  if (rawSer === '') rawSer = '1'
                                  const pn = parseInt(String(rawSer).trim(), 10)
                                  const patch = { rep_series_count: rawSer }
                                  if (!Number.isFinite(pn) || pn < 2) patch.intra_rep_rest_sec = ''
                                  patchComboSlotRow(idx, patch)
                                }}
                              />
                            </div>
                          ) : null}
                          {slotAdv === 'timed' || showInterSeriesPause ? (
                            <div className="form-row" style={{ marginBottom: 0 }}>
                              <label className="form-label" style={{ fontSize: '10px' }}>
                                {intraLabel}
                              </label>
                              <input
                                type="number"
                                min={0}
                                className="form-input"
                                style={comboTinyNumberInputSx}
                                placeholder="–"
                                value={row.intra_rep_rest_sec || ''}
                                onChange={(e) => patchComboSlotRow(idx, { intra_rep_rest_sec: e.target.value })}
                              />
                            </div>
                          ) : null}
                          <div className="form-row" style={{ marginBottom: 0 }}>
                            <label className="form-label" style={{ fontSize: '10px' }}>
                              Wechsel (s)
                            </label>
                            <input
                              type="number"
                              min={0}
                              className="form-input"
                              style={comboTinyNumberInputSx}
                              placeholder="–"
                              value={row.transition_after_sec || ''}
                              onChange={(e) => patchComboSlotRow(idx, { transition_after_sec: e.target.value })}
                            />
                          </div>
                        </div>
                        {showMultiSeries && serienCountUi < 2 ? (
                          <p style={{ fontSize: '10px', color: 'var(--text3)', margin: '6px 0 0', lineHeight: 1.38 }}>
                            <strong>Wechsel (s)</strong> = Pause bis zur <strong>nächsten Station</strong>. Feld „Pause zw.
                            Serien“ erscheint erst ab&nbsp;2 Serien (sonst keine Pause zwischen zwei Blöcken nötig).
                          </p>
                        ) : null}
                      </div>
                    )
                  })}
                  <div
                    onDragOver={(e) => {
                      if (!e.dataTransfer?.types?.includes?.(DND_EXERCISE_COMBO_STATION)) return
                      e.preventDefault()
                      e.dataTransfer.dropEffect = 'move'
                    }}
                    onDrop={(e) => {
                      const rawFrom = e.dataTransfer.getData(DND_EXERCISE_COMBO_STATION)
                      const fromI = parseInt(rawFrom, 10)
                      e.preventDefault()
                      setComboDropTargetIx(null)
                      if (!Number.isFinite(fromI)) return
                      const len = (formData.combination_slots || []).length
                      reorderCombinationSlots(fromI, len)
                    }}
                    style={{
                      padding: '10px',
                      textAlign: 'center',
                      fontSize: '11px',
                      color: 'var(--text3)',
                      border: '1px dashed var(--border)',
                      borderRadius: '10px',
                      marginBottom: '8px',
                    }}
                  >
                    Hier ablegen zum Anhängen am Ende der Reihenfolge
                  </div>
                  <button
                    type="button"
                    className="btn btn-secondary"
                    style={{ fontSize: '12px', marginTop: '4px' }}
                    onClick={() => updateFormField('combination_slots', [...(formData.combination_slots || []), emptyComboSlotRow()])}
                  >
                    + Station
                  </button>
                </div>
                <div className="form-row">
                  <label className="form-label">Ablaufprofil (Runden &amp; global)</label>
                  <CombinationMethodProfileEditor
                    methodArchetype={formData.method_archetype || ''}
                    methodProfileJson={formData.method_profile_json || '{}'}
                    onChangeMethodProfileJson={(s) => updateFormField('method_profile_json', s)}
                    comboSlotsOutline={(formData.combination_slots || []).map((r, i) => ({
                      slot_index: i,
                      title: r.title || '',
                    }))}
                    omitPerSlotTiming
                  />
                </div>
              </>
            ) : (
              <p className="exercise-form-panel__hint" style={{ margin: 0 }}>
                Wähle unter <strong>Stammdaten</strong> die Art „Kombinationsübung“, um Stationen zu planen.
              </p>
            )}
          </ExerciseFormPanel>

          <ExerciseFormPanel
            tab="anleitung"
            activeTab={activeFormTab}
            tone="guide"
            title="Anleitung"
            hint="Ziel, Ablauf und Hinweise — Medien kannst du in die Texte einbetten (Symbolleiste)."
          >
          <div
            style={{
              display: 'flex',
              flexWrap: 'wrap',
              gap: '8px',
              marginBottom: '12px',
              alignItems: 'center',
            }}
          >
            <button
              type="button"
              className="btn btn-secondary"
              style={{ fontSize: '12px' }}
              disabled={aiSuggestBusy}
              onClick={() => runExerciseAiInstructionRewrite()}
            >
              KI: Anleitung überarbeiten
            </button>
            <span style={{ fontSize: '12px', color: 'var(--text3)' }}>
              Überarbeitet Ziel, Durchführung, Vorbereitung und Trainer-Hinweise — prägnant und strukturiert. Vorschau
              im Dialog; nichts wird automatisch gespeichert.
            </span>
          </div>
          <div className="form-row">
            <label className="form-label">Ziel *</label>
            <RichTextEditor
              value={formData.goal}
              onChange={(html) => updateFormField('goal', html)}
              placeholder="Trainingsziel"
              minHeight="120px"
              inlineExerciseId={isEdit ? exerciseId : null}
              linkedExerciseMedia={isEdit ? mediaList : []}
              onExerciseMediaListChanged={refreshMedia}
            />
          </div>

          <div className="form-row">
            <label className="form-label">Durchführung *</label>
            <RichTextEditor
              value={formData.execution}
              onChange={(html) => updateFormField('execution', html)}
              placeholder="Ablauf Schritt für Schritt"
              minHeight="180px"
              inlineExerciseId={isEdit ? exerciseId : null}
              linkedExerciseMedia={isEdit ? mediaList : []}
              onExerciseMediaListChanged={refreshMedia}
            />
          </div>

          <div className="form-row">
            <label className="form-label">Vorbereitung / Aufbau</label>
            <RichTextEditor
              value={formData.preparation}
              onChange={(html) => updateFormField('preparation', html)}
              placeholder="Matten, Raum, …"
              minHeight="100px"
              inlineExerciseId={isEdit ? exerciseId : null}
              linkedExerciseMedia={isEdit ? mediaList : []}
              onExerciseMediaListChanged={refreshMedia}
            />
          </div>

          <div className="form-row">
            <label className="form-label">Hinweise für Trainer</label>
            <RichTextEditor
              value={formData.trainer_notes}
              onChange={(html) => updateFormField('trainer_notes', html)}
              placeholder="Sicherheit, Varianten-Hinweise, …"
              minHeight="100px"
              inlineExerciseId={isEdit ? exerciseId : null}
              linkedExerciseMedia={isEdit ? mediaList : []}
              onExerciseMediaListChanged={refreshMedia}
            />
          </div>
          </ExerciseFormPanel>

          <ExerciseFormPanel
            tab="einordnung"
            activeTab={activeFormTab}
            tone="classify"
            title="Einordnung"
            hint="Fokus, Stile, Zielgruppen und Fähigkeiten für Suche, Filter und Skill-Profil."
          >
            <div
              style={{
                display: 'flex',
                flexWrap: 'wrap',
                gap: '8px',
                marginBottom: '12px',
                alignItems: 'center',
              }}
            >
              <button
                type="button"
                className="btn btn-secondary"
                style={{ fontSize: '12px' }}
                disabled={aiSuggestBusy}
                onClick={() => runExerciseAiSuggestion('skills')}
              >
                KI: Fähigkeiten
              </button>
              <button
                type="button"
                className="btn btn-secondary"
                style={{ fontSize: '12px' }}
                disabled={aiSuggestBusy}
                onClick={() => runExerciseAiSuggestion('both')}
              >
                KI: Kurzfassung und Fähigkeiten
              </button>
              <span style={{ fontSize: '12px', color: 'var(--text3)' }}>
                Benötigt Ziel oder Durchführung sowie optional{' '}
                <button type="button" className="exercise-form-inline-tab-link" onClick={() => setActiveFormTab('anleitung')}>
                  Anleitung
                </button>
                · Es öffnet ein Dialogfeld mit Vorschau; Übernahme wählweise pro Teil. Speichern nur über die Aktionsleiste.
              </span>
            </div>

          <section className="exercise-form-meta-panel" aria-label="Klassifikation">
            <div className="exercise-form-meta-panel__grid">
              <ExerciseCatalogAssocEditor
                title="Fokusbereiche"
                rows={formData.focus_areas_multi}
                setRows={(r) => updateFormField('focus_areas_multi', r)}
                options={focusAreas}
                idKey="focus_area_id"
                emptyLabel="Optional — „+ Eintrag“."
              />

              <ExerciseCatalogAssocEditor
                title="Stilrichtungen"
                rows={formData.training_styles_multi}
                setRows={(r) => updateFormField('training_styles_multi', r)}
                options={styleDirections.map((sd) => ({
                  ...sd,
                  name: sd.parent_style_name ? `${sd.name} (${sd.parent_style_name})` : sd.name,
                }))}
                idKey="training_style_id"
                emptyLabel="Optional."
              />

              <ExerciseCatalogAssocEditor
                title="Trainingsstil"
                rows={formData.training_types_multi}
                setRows={(r) => updateFormField('training_types_multi', r)}
                options={trainingTypes}
                idKey="training_type_id"
                emptyLabel="Optional."
              />

              <ExerciseCatalogAssocEditor
                title="Zielgruppen"
                rows={formData.target_groups_multi}
                setRows={(r) => updateFormField('target_groups_multi', r)}
                options={targetGroups}
                idKey="target_group_id"
                emptyLabel="Optional."
                showPrimary={false}
              />
            </div>

            <ExerciseSkillsEditor
              rows={formData.skills}
              skillsCatalog={skillsCatalog}
              skillPick={skillPick}
              onSkillPickChange={setSkillPick}
              onAdd={addSkillRow}
              onRemove={removeSkillRow}
              onUpdateField={updateSkillField}
            />
          </section>
          </ExerciseFormPanel>

          {isEdit && formData.exercise_kind !== 'combination' ? (
            <ExerciseFormPanel
              tab="varianten"
              activeTab={activeFormTab}
              tone="variants"
              title="Übungsvarianten"
              hint="Pro Durchgang eine Variante. Änderungen werden mit Speichern in der Aktionsleiste mitgesichert."
            >
            {variants.length > 0 && (
              <div className="form-row">
                <label className="form-label" htmlFor="variant-edit-select">
                  Variante auswählen
                </label>
                <select
                  id="variant-edit-select"
                  className="form-input"
                  value={
                    variantEditSelection === 'new'
                      ? 'new'
                      : variantEditSelection == null
                        ? ''
                        : String(variantEditSelection)
                  }
                  onChange={(e) => {
                    const val = e.target.value
                    if (val === '') setVariantEditSelection(null)
                    else if (val === 'new') setVariantEditSelection('new')
                    else setVariantEditSelection(parseInt(val, 10))
                  }}
                >
                  <option value="">— nicht bearbeiten —</option>
                  {variants.map((v) => (
                    <option key={v.id} value={v.id}>
                      {(v.variant_name && String(v.variant_name).trim()) || `Variante #${v.id}`}
                    </option>
                  ))}
                  <option value="new">+ Neue Variante anlegen…</option>
                </select>
              </div>
            )}

            {variants.length === 0 && (
              <p style={{ fontSize: '13px', color: 'var(--text3)', marginBottom: '10px' }}>
                Noch keine Varianten – optional für andere Ausführung, Dauer oder Material in Planung und Training.
              </p>
            )}

            {variants.length === 0 && variantEditSelection !== 'new' && (
              <button type="button" className="btn btn-secondary" onClick={() => setVariantEditSelection('new')}>
                Erste Variante anlegen
              </button>
            )}

            {variantEditSelection === 'new' && (
              <div
                className="exercise-variant-single-form"
                style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}
              >
                <h3 style={{ marginTop: 0, fontSize: '1rem' }}>Neue Variante</h3>
                <ExerciseVariantFields
                  row={variantDraft}
                  onPatch={(patch) => {
                    setFormDirty(true)
                    setVariantDraft((d) => ({ ...d, ...patch }))
                  }}
                  prerequisiteOthers={variants}
                  rteMinHeight="110px"
                  inlineExerciseId={isEdit ? exerciseId : null}
                  linkedExerciseMedia={isEdit ? mediaList : []}
                  onExerciseMediaListChanged={refreshMedia}
                />
                <button
                  type="button"
                  className="btn btn-primary"
                  style={{ marginTop: '10px' }}
                  disabled={variantBusy}
                  onClick={handleCreateVariantClick}
                >
                  {variantBusy ? 'Anlegen…' : 'Variante anlegen'}
                </button>
                <p className="exercise-form-panel__hint" style={{ marginTop: '8px', marginBottom: 0 }}>
                  Alternativ reicht „Speichern“ in der Aktionsleiste — der Entwurf wird dann mitgesichert.
                </p>
              </div>
            )}

            {selectedVariantForEdit && (
              <div
                className="exercise-variant-single-form"
                style={{ marginTop: '14px', paddingTop: '14px', borderTop: '1px solid var(--border)' }}
              >
                <div
                  style={{
                    display: 'flex',
                    flexWrap: 'wrap',
                    gap: '8px',
                    alignItems: 'center',
                    marginBottom: '12px',
                  }}
                >
                  <span style={{ fontSize: '12px', color: 'var(--text3)' }}>
                    Pos. {selectedVariantIdx + 1} von {variants.length}
                  </span>
                  <button
                    type="button"
                    className="btn btn-secondary"
                    style={{ fontSize: '11px', padding: '4px 8px' }}
                    disabled={variantBusy || selectedVariantIdx <= 0}
                    onClick={() => moveVariantRow(selectedVariantIdx, -1)}
                  >
                    Nach oben
                  </button>
                  <button
                    type="button"
                    className="btn btn-secondary"
                    style={{ fontSize: '11px', padding: '4px 8px' }}
                    disabled={variantBusy || selectedVariantIdx >= variants.length - 1}
                    onClick={() => moveVariantRow(selectedVariantIdx, 1)}
                  >
                    Nach unten
                  </button>
                  <button
                    type="button"
                    className="btn btn-secondary"
                    style={{ marginLeft: 'auto', fontSize: '12px' }}
                    disabled={variantSavingId === selectedVariantForEdit.id || variantBusy}
                    onClick={() => saveVariantRow(selectedVariantForEdit)}
                    title="Optional — Änderungen werden auch über die Aktionsleiste gespeichert"
                  >
                    {variantSavingId === selectedVariantForEdit.id ? 'Speichern…' : 'Variante jetzt speichern'}
                  </button>
                  <button
                    type="button"
                    className="btn"
                    style={{ fontSize: '12px', background: 'var(--danger)', color: '#fff', border: 'none' }}
                    disabled={variantBusy}
                    onClick={() => deleteVariantRow(selectedVariantForEdit.id)}
                  >
                    Löschen
                  </button>
                </div>
                <ExerciseVariantFields
                  row={selectedVariantForEdit}
                  onPatch={(patch) => updateVariantField(selectedVariantForEdit.id, patch)}
                  prerequisiteOthers={variants.filter((o) => o.id !== selectedVariantForEdit.id)}
                  rteMinHeight="110px"
                  inlineExerciseId={isEdit ? exerciseId : null}
                  linkedExerciseMedia={isEdit ? mediaList : []}
                  onExerciseMediaListChanged={refreshMedia}
                />
              </div>
            )}

            {variants.length > 0 && variantEditSelection == null && (
              <p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: '12px', marginBottom: 0 }}>
                Wähle eine Variante zum Bearbeiten oder „Neue Variante anlegen…“.
              </p>
            )}
            </ExerciseFormPanel>
          ) : null}

          {isEdit ? (
            <ExerciseFormPanel
              tab="medien"
              activeTab={activeFormTab}
              tone="media"
              title="Medien & Erweiterungen"
              hint="Verknüpfte Dateien, Progressionsgraph und Medienarchiv."
            >
              <div className="exercise-form-subsection exercise-form-subsection--media">
                <h4 className="exercise-form-subsection__title">Medien</h4>
          <p style={{ color: 'var(--text2)', fontSize: '13px', marginBottom: '6px' }}>
            Neue Uploads oder Embeds über die Textfeld-Symbolleiste („Medien im Text“ / „Embed im Text“). Hier
            verwaltest du Verknüpfungen — Kachel in ein Textfeld ziehen, um sie an der Cursorposition einzufügen
            (mittlere Darstellung).
          </p>
          <p style={{ color: 'var(--text3)', fontSize: '12px', marginTop: 0 }}>
            Max. 10 Medien pro Übung.
          </p>
          <div
            style={{
              display: 'flex',
              flexWrap: 'wrap',
              gap: '8px',
              alignItems: 'center',
              marginTop: '10px',
            }}
          >
            <button type="button" className="btn btn-secondary" onClick={() => setArchiveOpen(true)}>
              Aus Archiv verknüpfen…
            </button>
            <Link to="/media" className="btn btn-secondary" style={{ textDecoration: 'none' }}>
              Medienbibliothek
            </Link>
          </div>
          {mediaList.length > 0 && (
            <ul className="exercise-edit-media-strip">
              {mediaList.map((m, idx) => {
                const cap =
                  (m.title || '').trim() ||
                  (m.original_filename || '').trim() ||
                  (m.embed_url ? String(m.embed_url).replace(/^https?:\/\//i, '').slice(0, 80) : '')
                const sub = [m.media_type, m.embed_platform].filter(Boolean).join(' · ') || 'Medium'
                const payloadCaption = (
                  [m.title, m.original_filename].find((x) => typeof x === 'string' && x.trim()) || ''
                ).trim()
                return (
                  <li key={m.id} className="exercise-edit-media-strip__item">
                    <div className="exercise-edit-media-strip__lead">
                      {!m.embed_url ? (
                        <ExerciseMediaThumbTile exerciseId={exerciseId} media={m} onOpenPreview={setMediaPreview} size={76} />
                      ) : (
                        <div
                          className="exercise-edit-media-strip__embed-badge exercise-edit-media-strip__embed-badge--solo"
                          aria-hidden
                        >
                          {m.embed_platform || 'Embed'}
                        </div>
                      )}
                      <div
                        className="exercise-edit-media-strip__handle"
                        title="Mit Drag und Drop in ein Textfeld ziehen"
                        draggable
                        onDragStart={(e) => {
                          try {
                            e.dataTransfer.setData(
                              SHINKAN_EXERCISE_MEDIA_DRAG_MIME,
                              buildExerciseMediaDragPayload(m.id, payloadCaption),
                            )
                            e.dataTransfer.effectAllowed = 'copy'
                          } catch (_) {
                            /* ignore */
                          }
                        }}
                      >
                        ⣿<span className="exercise-edit-media-strip__handle-text"> Ziehen</span>
                      </div>
                    </div>
                    <div className="exercise-edit-media-strip__body">
                      <div className="exercise-edit-media-strip__headline">
                        <span style={{ fontSize: '12px', color: 'var(--text3)' }}>
                          #{m.id} · {sub}
                        </span>
                      </div>
                      <div style={{ fontSize: '13px', color: 'var(--text2)', lineHeight: 1.35 }}>{cap || '—'}</div>
                      <div className="exercise-edit-media-strip__toolbar">
                        <input
                          type="text"
                          className="form-input exercise-edit-media-strip__title"
                          placeholder="Titel (wird in der Vorschau und im Platzhalter genutzt)"
                          value={(mediaFields[m.id] || {}).title ?? ''}
                          onChange={(e) =>
                            setMediaFields((prev) => ({
                              ...prev,
                              [m.id]: {
                                title: e.target.value,
                              },
                            }))
                          }
                        />
                      </div>
                      <div className="exercise-edit-media-strip__actions">
                        {mediaList.length > 1 && (
                          <>
                            <button
                              type="button"
                              className="btn btn-secondary"
                              style={{ fontSize: '11px', padding: '4px 8px' }}
                              disabled={idx === 0}
                              onClick={() => moveMediaRow(idx, -1)}
                              title="Nach oben"
                            >
                              ↑
                            </button>
                            <button
                              type="button"
                              className="btn btn-secondary"
                              style={{ fontSize: '11px', padding: '4px 8px' }}
                              disabled={idx >= mediaList.length - 1}
                              onClick={() => moveMediaRow(idx, 1)}
                              title="Nach unten"
                            >
                              ↓
                            </button>
                          </>
                        )}
                        <button
                          type="button"
                          className="btn btn-primary"
                          style={{ fontSize: '12px', padding: '4px 10px' }}
                          disabled={mediaSavingId === m.id}
                          onClick={() => saveMediaMeta(m.id)}
                        >
                          {mediaSavingId === m.id ? '…' : 'Speichern'}
                        </button>
                        <button
                          type="button"
                          className="btn btn-secondary"
                          style={{ fontSize: '12px', padding: '4px 10px' }}
                          onClick={() => handleDeleteMedia(m.id)}
                        >
                          Entfernen
                        </button>
                      </div>
                    </div>
                  </li>
                )
              })}
            </ul>
          )}
          <p style={{ color: 'var(--text3)', fontSize: '12px', marginTop: mediaList.length ? '12px' : 0 }}>
            Verknüpfungen bleiben nötig (u. a. Zugriff, Orphan-Hinweise): Im Fließtext verweist du gezielt über
            Platzhalter. Ohne Verknüpfung gäbe es keine exercise_media-ID zum Einbetten.
          </p>
              </div>

              {formData.exercise_kind !== 'combination' ? (
                <div className="exercise-form-subsection exercise-form-subsection--graph">
                  <h4 className="exercise-form-subsection__title">Progressionsgraph</h4>
                  <p className="exercise-form-subsection__hint">Übergänge zu anderen Übungen für Progressions-Serien.</p>
                  <ExerciseProgressionGraphPanel anchorExerciseId={exerciseId} anchorTitle={formData.title} />
                </div>
              ) : null}
          {archiveOpen && (
            <div
              role="dialog"
              aria-modal="true"
              aria-label="Medienarchiv"
              style={{
                position: 'fixed',
                inset: 0,
                background: 'rgba(0,0,0,0.5)',
                zIndex: 1000,
                overflow: 'auto',
                padding: '16px',
              }}
              onClick={() => setArchiveOpen(false)}
              onKeyDown={(e) => e.key === 'Escape' && setArchiveOpen(false)}
            >
              <div
                className="card"
                style={{
                  maxWidth: 560,
                  margin: '4vh auto',
                  maxHeight: '88vh',
                  overflow: 'auto',
                  position: 'relative',
                }}
                onClick={(e) => e.stopPropagation()}
              >
                <h3 style={{ marginTop: 0, fontSize: '1.05rem' }}>Medienarchiv</h3>
                <input
                  type="search"
                  className="form-input"
                  placeholder="Suche Dateiname…"
                  value={archiveQ}
                  onChange={(e) => setArchiveQ(e.target.value)}
                  style={{ marginBottom: '8px' }}
                />
                {archiveLoading && <p style={{ fontSize: '13px', color: 'var(--text3)' }}>Laden…</p>}
                {archiveError && <p style={{ fontSize: '13px', color: 'var(--danger)' }}>{archiveError}</p>}
                {!archiveLoading && !archiveError && archiveItems.length === 0 && (
                  <p style={{ fontSize: '13px', color: 'var(--text3)' }}>Keine Treffer.</p>
                )}
                <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
                  {archiveItems.map((a) => {
                    const already = linkedArchiveAssetIds.has(a.id)
                    return (
                    <li
                      key={a.id}
                      style={{
                        display: 'flex',
                        gap: '10px',
                        alignItems: 'center',
                        padding: '8px 0',
                        borderBottom: '1px solid var(--border)',
                      }}
                    >
                      <div
                        style={{
                          width: 56,
                          height: 56,
                          flexShrink: 0,
                          borderRadius: '6px',
                          overflow: 'hidden',
                          background: 'var(--surface2, rgba(127,127,127,0.12))',
                          border: '1px solid var(--border)',
                          display: 'flex',
                          alignItems: 'center',
                          justifyContent: 'center',
                        }}
                      >
                        {a.mime_type?.startsWith('image/') ? (
                          <img
                            alt=""
                            src={resolveMediaAssetFileUrl(a.id)}
                            style={{ width: '100%', height: '100%', objectFit: 'cover' }}
                          />
                        ) : a.mime_type?.startsWith('video/') ? (
                          <span style={{ fontSize: '18px', opacity: 0.75 }} aria-hidden>
                            ▶
                          </span>
                        ) : (
                          <span style={{ fontSize: '10px', color: 'var(--text2)', padding: '2px', textAlign: 'center' }}>
                            PDF
                          </span>
                        )}
                      </div>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div style={{ fontSize: '13px', fontWeight: 600, wordBreak: 'break-word' }}>
                          {a.original_filename || `Asset #${a.id}`}
                        </div>
                        <div style={{ fontSize: '11px', color: 'var(--text3)', marginTop: '2px' }}>
                          {a.visibility} · {a.mime_type || '—'}{' '}
                          {a.byte_size != null ? `· ${(a.byte_size / 1024).toFixed(0)} KB` : ''}
                        </div>
                      </div>
                      <button
                        type="button"
                        className="btn btn-primary"
                        style={{ fontSize: '12px' }}
                        disabled={already}
                        title={already ? 'Schon mit dieser Übung verknüpft' : ''}
                        onClick={() => !already && attachFromArchive(a.id)}
                      >
                        {already ? 'Bereits verknüpft' : 'Verknüpfen'}
                      </button>
                    </li>
                    )
                  })}
                </ul>
                <div style={{ marginTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
                  <button type="button" className="btn btn-secondary" onClick={() => setArchiveOpen(false)}>
                    Schließen
                  </button>
                </div>
              </div>
            </div>
          )}
          {mediaPreview && (
            <MediaPreviewModal
              title={(mediaPreview.title || '').trim() || mediaPreview.original_filename || `Medium #${mediaPreview.id}`}
              media={mediaPreview}
              fileUrl={mediaPreview.embed_url ? null : resolveExerciseMediaFileUrl(exerciseId, mediaPreview)}
              onClose={() => setMediaPreview(null)}
              onReport={
                !mediaPreview.asset_legal_hold_active
                  ? () => {
                      setReportTarget(mediaPreview)
                      setMediaPreview(null)
                    }
                  : null
              }
            />
          )}
          {reportTarget && (
            <ReportContentModal
              targetType="media_asset"
              targetId={reportTarget.media_asset_id || reportTarget.id}
              targetLabel={reportTarget.title || reportTarget.original_filename || `Medium #${reportTarget.id}`}
              onClose={() => setReportTarget(null)}
            />
          )}
            </ExerciseFormPanel>
          ) : null}

        </form>
      </div>

      {aiSuggestionPreview &&
        (() => {
          const p = aiSuggestionPreview
          const summaryBoxSx = {
            padding: '10px 12px',
            borderRadius: '8px',
            border: '1px solid var(--border)',
            background: 'var(--surface2)',
            fontSize: '13px',
            lineHeight: 1.45,
            whiteSpace: 'pre-wrap',
            wordBreak: 'break-word',
            minHeight: '72px',
          }
          const canApplySomething =
            (p.applySummary && p.summaryAfterHtml) ||
            p.skillChoices.some((c) => c.include) ||
            (p.instructionChoices || []).some((c) => c.include && c.afterHtml)
          const dialogTitle =
            p.instructionsRequested
              ? 'KI: Anleitung überarbeiten'
              : 'KI-Vorschlag übernehmen'
          return (
            <div
              role="dialog"
              aria-modal="true"
              aria-label="KI-Vorschlag prüfen"
              style={{
                position: 'fixed',
                inset: 0,
                background: 'rgba(0,0,0,0.5)',
                zIndex: 1001,
                overflow: 'auto',
                padding: '16px',
              }}
              onClick={() => discardExerciseAiSuggestionPreview()}
              onKeyDown={(e) => e.key === 'Escape' && discardExerciseAiSuggestionPreview()}
            >
              <div
                className="card"
                style={{
                  maxWidth: 760,
                  margin: '3vh auto',
                  maxHeight: '92vh',
                  overflow: 'auto',
                  position: 'relative',
                }}
                onClick={(e) => e.stopPropagation()}
              >
                <h3 style={{ marginTop: 0, fontSize: '1.1rem', marginBottom: '6px' }}>{dialogTitle}</h3>
                <p style={{ fontSize: '13px', color: 'var(--text3)', marginTop: 0, marginBottom: '16px' }}>
                  {p.instructionsRequested
                    ? 'Vergleichen und nur die gewünschten Felder übernehmen. Eingebettete Medien bleiben erhalten, wenn die KI sie nicht erwähnt.'
                    : 'Vergleichen und nur die gewünschten Teile übernehmen. Es werden keine Daten automatisch gespeichert.'}
                </p>

                {p.hasInstructionChoices ? (
                  <section style={{ marginBottom: '20px' }} aria-labelledby="ai-preview-instructions-heading">
                    <div
                      id="ai-preview-instructions-heading"
                      style={{ fontWeight: 600, fontSize: '0.95rem', marginBottom: '10px' }}
                    >
                      Anleitung ({p.instructionChoices.length}{' '}
                      {p.instructionChoices.length === 1 ? 'Feld' : 'Felder'})
                    </div>
                    {p.instructionChoices.map((c) => (
                      <div
                        key={c.key}
                        style={{
                          border: '1px solid var(--border)',
                          borderRadius: '8px',
                          padding: '12px',
                          marginBottom: '12px',
                          background: 'var(--surface)',
                        }}
                      >
                        <label
                          style={{
                            display: 'flex',
                            alignItems: 'center',
                            gap: '8px',
                            marginBottom: '10px',
                            cursor: 'pointer',
                            fontSize: '14px',
                            fontWeight: 600,
                          }}
                        >
                          <input
                            type="checkbox"
                            checked={c.include}
                            onChange={(e) =>
                              setAiSuggestionPreview((prev) =>
                                prev
                                  ? {
                                      ...prev,
                                      instructionChoices: prev.instructionChoices.map((x) =>
                                        x.key === c.key ? { ...x, include: e.target.checked } : x,
                                      ),
                                    }
                                  : prev,
                              )
                            }
                          />
                          {c.label} übernehmen
                        </label>
                        <div
                          style={{
                            display: 'grid',
                            gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)',
                            gap: '12px',
                          }}
                        >
                          <div>
                            <div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>
                              Aktuell (Plaintext)
                            </div>
                            <div style={summaryBoxSx}>{c.beforePlain || '(leer)'}</div>
                          </div>
                          <div>
                            <div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>
                              KI-Vorschlag
                            </div>
                            <div
                              style={{
                                ...summaryBoxSx,
                                borderColor: 'var(--accent-dark, rgba(29,158,117,0.45))',
                              }}
                            >
                              {c.afterPlain || '(leer)'}
                            </div>
                          </div>
                        </div>
                      </div>
                    ))}
                  </section>
                ) : null}

                {p.hasSummaryProposal ? (
                  <section style={{ marginBottom: '20px' }} aria-labelledby="ai-preview-summary-heading">
                    <div
                      id="ai-preview-summary-heading"
                      style={{ fontWeight: 600, fontSize: '0.95rem', marginBottom: '10px' }}
                    >
                      Kurzfassung
                    </div>
                    <label
                      style={{
                        display: 'flex',
                        alignItems: 'center',
                        gap: '8px',
                        marginBottom: '10px',
                        cursor: 'pointer',
                        fontSize: '14px',
                      }}
                    >
                      <input
                        type="checkbox"
                        checked={p.applySummary}
                        onChange={(e) =>
                          setAiSuggestionPreview((prev) =>
                            prev ? { ...prev, applySummary: e.target.checked } : prev,
                          )
                        }
                      />
                      Kurzfassung durch Vorschlag ersetzen (bestehende Kurzbeschreibung wird überschrieben)
                    </label>
                    <div
                      style={{
                        display: 'grid',
                        gridTemplateColumns: 'minmax(0,1fr) minmax(0,1fr)',
                        gap: '12px',
                      }}
                    >
                      <div>
                        <div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>
                          Aktuell (ohne Formatierung)
                        </div>
                        <div style={summaryBoxSx}>{p.summaryBeforePlain || '(leer)'}</div>
                      </div>
                      <div>
                        <div style={{ fontSize: '12px', color: 'var(--text3)', marginBottom: '4px' }}>KI-Vorschlag</div>
                        <div style={{ ...summaryBoxSx, borderColor: 'var(--accent-dark, rgba(29,158,117,0.45))' }}>
                          {p.summaryAfterPlain || '(leer)'}
                        </div>
                      </div>
                    </div>
                  </section>
                ) : null}

                {p.skillsRequested ? (
                  <section aria-labelledby="ai-preview-skills-heading">
                    <div
                      style={{
                        display: 'flex',
                        flexWrap: 'wrap',
                        alignItems: 'center',
                        justifyContent: 'space-between',
                        gap: '8px',
                        marginBottom: '10px',
                      }}
                    >
                      <div id="ai-preview-skills-heading" style={{ fontWeight: 600, fontSize: '0.95rem' }}>
                        Fähigkeiten ({p.skillChoices.length}
                        {p.skillChoices.length === 1 ? ' Vorschlag' : ' Vorschläge'})
                      </div>
                      {p.skillChoices.length > 0 ? (
                        <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap' }}>
                          <button
                            type="button"
                            className="btn btn-secondary"
                            style={{ fontSize: '11px', padding: '4px 8px' }}
                            onClick={() =>
                              setAiSuggestionPreview((prev) =>
                                prev
                                  ? {
                                      ...prev,
                                      skillChoices: prev.skillChoices.map((x) => ({ ...x, include: true })),
                                    }
                                  : prev,
                              )
                            }
                          >
                            Alle auswählen
                          </button>
                          <button
                            type="button"
                            className="btn btn-secondary"
                            style={{ fontSize: '11px', padding: '4px 8px' }}
                            onClick={() =>
                              setAiSuggestionPreview((prev) =>
                                prev
                                  ? {
                                      ...prev,
                                      skillChoices: prev.skillChoices.map((x) => ({ ...x, include: false })),
                                    }
                                  : prev,
                              )
                            }
                          >
                            Alle abwählen
                          </button>
                        </div>
                      ) : null}
                    </div>
                    {p.skillChoices.length === 0 ? (
                      <p style={{ fontSize: '13px', color: 'var(--text3)', margin: 0 }}>
                        Keine passenden Fähigkeiten — der Katalog-Vorschlag war leer oder enthielt nur ungültige IDs.
                      </p>
                    ) : (
                      <ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
                        {p.skillChoices.map((c) => (
                          <li
                            key={c.key}
                            style={{
                              border: '1px solid var(--border)',
                              borderRadius: '8px',
                              padding: '10px 12px',
                              marginBottom: '10px',
                              background: 'var(--surface)',
                            }}
                          >
                            <label
                              style={{
                                display: 'flex',
                                gap: '10px',
                                alignItems: 'flex-start',
                                cursor: 'pointer',
                                margin: 0,
                              }}
                            >
                              <input
                                type="checkbox"
                                checked={c.include}
                                onChange={() =>
                                  setAiSuggestionPreview((prev) =>
                                    prev
                                      ? {
                                          ...prev,
                                          skillChoices: prev.skillChoices.map((x) =>
                                            x.skill_id === c.skill_id ? { ...x, include: !x.include } : x,
                                          ),
                                        }
                                      : prev,
                                  )
                                }
                                style={{ marginTop: '4px' }}
                              />
                              <div style={{ flex: 1, minWidth: 0 }}>
                                <div style={{ fontWeight: 600, fontSize: '13px', marginBottom: '6px' }}>
                                  {c.kind === 'add' ? 'Neu hinzufügen' : 'Bestehende Zeile aktualisieren'}
                                </div>
                                {c.kind === 'update' && c.before ? (
                                  <div style={{ fontSize: '12px', lineHeight: 1.5 }}>
                                    <div style={{ color: 'var(--text3)', marginBottom: '2px' }}>Bisher</div>
                                    <div style={{ marginBottom: '8px' }}>
                                      {describeExerciseSkillRowForPreview(c.before, skillsCatalog)}
                                    </div>
                                    <div style={{ color: 'var(--text3)', marginBottom: '2px' }}>Nach KI-Vorschlag</div>
                                    <div>{describeExerciseSkillRowForPreview(c.after, skillsCatalog)}</div>
                                  </div>
                                ) : (
                                  <div style={{ fontSize: '13px', lineHeight: 1.5 }}>
                                    {describeExerciseSkillRowForPreview(c.after, skillsCatalog)}
                                  </div>
                                )}
                              </div>
                            </label>
                          </li>
                        ))}
                      </ul>
                    )}
                  </section>
                ) : null}

                <div
                  style={{
                    marginTop: '20px',
                    paddingTop: '14px',
                    borderTop: '1px solid var(--border)',
                    display: 'flex',
                    justifyContent: 'flex-end',
                    flexWrap: 'wrap',
                    gap: '10px',
                  }}
                >
                  <button type="button" className="btn btn-secondary" onClick={discardExerciseAiSuggestionPreview}>
                    Abbrechen
                  </button>
                  <button
                    type="button"
                    className="btn btn-primary"
                    disabled={!canApplySomething}
                    onClick={() => applyExerciseAiSuggestionPreview()}
                  >
                    Ausgewähltes übernehmen
                  </button>
                </div>
              </div>
            </div>
          )
        })()}

      <ExercisePickerModal
        open={comboStationPickerIx !== null}
        onClose={() => setComboStationPickerIx(null)}
        exerciseKindAny={['simple']}
        multiSelect
        enableQuickCreateDraft
        onSelectExercises={(picked) => {
          if (comboStationPickerIx === null) return
          mergePickedExercisesIntoSlot(comboStationPickerIx, picked)
          setComboStationPickerIx(null)
        }}
      />

      <p style={{ fontSize: '12px', color: 'var(--text3)', marginTop: '16px' }}>
        <strong>KI-Unterstützung:</strong> OpenRouter-Vorschläge für Kurzfassung, Fähigkeiten und Anleitung
        (<code>suggestExerciseAi</code> / <code>regenerateExerciseAi</code>). Übernahme im Dialog ins Formular; Speichern
        wie gewohnt.
      </p>
      <UnsavedChangesPrompt
        blocker={blocker}
        isBusy={saving}
        onSave={handleUnsavedDialogSave}
        onDiscardWithoutSave={() => setFormDirty(false)}
        detail="Du hast ungespeicherte Änderungen vorgenommen. Möchtest du die Seite wirklich verlassen?"
      />
    </PageFormEditorChrome>
  )
}

export default ExerciseFormPageRoot
