import api from './api'
import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'

export function defaultSection(title = 'Hauptteil') {
  return { title, guidance_notes: '', items: [] }
}

/** Standard-`planLoc` für eine Ganzgruppen-Phase (Editor-Breakout-UI). */
export function defaultPlanLocWholeGroup(phaseOrderIndex = 0) {
  return {
    phaseKind: 'whole_group',
    phaseOrderIndex,
    parallelStreamOrderIndex: null,
    phaseTitle: null,
    phaseGuidanceNotes: null,
    streamTitle: null,
    streamNotes: null,
    streamAssignedTrainerProfileIds: null,
  }
}

/** Standard-`planLoc` für einen Stream innerhalb einer parallelen Phase. */
export function defaultPlanLocParallel(phaseOrderIndex, streamOrderIndex) {
  return {
    phaseKind: 'parallel',
    phaseOrderIndex,
    parallelStreamOrderIndex: streamOrderIndex,
    phaseTitle: null,
    phaseGuidanceNotes: null,
    streamTitle: null,
    streamNotes: null,
    streamAssignedTrainerProfileIds: null,
  }
}

export function planLocKey(pl) {
  if (!pl || !pl.phaseKind) return ''
  if (pl.phaseKind === 'whole_group') return `wg:${pl.phaseOrderIndex ?? 0}`
  return `par:${pl.phaseOrderIndex ?? 0}:${pl.parallelStreamOrderIndex ?? 0}`
}

export function maxPhaseOrderIndexFromSections(sections) {
  let m = -1
  for (const s of sections || []) {
    const pl = s?.planLoc
    if (!pl || typeof pl.phaseOrderIndex !== 'number') continue
    if (pl.phaseOrderIndex > m) m = pl.phaseOrderIndex
  }
  return m
}

/**
 * Eindeutige Ziele für die Zuordnung eines Abschnitts (Dropdown).
 * `template` ist ein vollständiges planLoc-Objekt zum Kopieren.
 */
export function buildPlanTargetOptions(sections) {
  const map = new Map()
  for (const s of sections || []) {
    const pl = s?.planLoc
    if (!pl?.phaseKind) continue
    if (pl.phaseKind === 'whole_group') {
      const po = pl.phaseOrderIndex ?? 0
      const k = `wg:${po}`
      if (!map.has(k)) {
        const title = pl.phaseTitle != null && String(pl.phaseTitle).trim() ? String(pl.phaseTitle).trim() : ''
        map.set(k, {
          key: k,
          label: title || `Ganzgruppe · Phase ${po}`,
          template: { ...pl },
        })
      }
    } else {
      const po = pl.phaseOrderIndex ?? 0
      const so = pl.parallelStreamOrderIndex ?? 0
      const k = `par:${po}:${so}`
      if (!map.has(k)) {
        const st = pl.streamTitle != null && String(pl.streamTitle).trim() ? String(pl.streamTitle).trim() : ''
        map.set(k, {
          key: k,
          label: st || `Parallel · Phase ${po} · Stream ${so}`,
          template: { ...pl },
        })
      }
    }
  }
  return [...map.values()].sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }))
}

/** Max. Streams pro paralleler Phase (UI + API-Schutz). */
export const MAX_PARALLEL_STREAMS_PER_PHASE = 5

/** Farben pro Stream-Index (max. 5 unterschiedliche Farbzyklen). */
export function parallelStreamVisual(streamOrderIndex) {
  const n = Math.max(0, Number(streamOrderIndex) || 0)
  const hues = [200, 135, 38, 285, 22]
  const h = hues[n % hues.length]
  return {
    border: `hsl(${h} 50% 36%)`,
    soft: `hsl(${h} 36% 94%)`,
    tabBg: `hsl(${h} 34% 92%)`,
    tabBgActive: `hsl(${h} 40% 82%)`,
  }
}

export function streamTabLabelFromIndices(sections, globalIndices) {
  const first = globalIndices?.[0]
  if (first === undefined || !sections?.[first]) return 'Stream'
  const pl = sections[first].planLoc
  const t = pl?.streamTitle != null && String(pl.streamTitle).trim() ? String(pl.streamTitle).trim() : ''
  if (t) return t
  const so = pl?.parallelStreamOrderIndex ?? 0
  return `Stream ${so + 1}`
}

/** Sortierte Stream-Indizes innerhalb einer parallelen Phase (für Reiter). */
export function streamsForParallelPhaseOrders(sections, phaseOrderIndex) {
  const set = new Set()
  const po = Number(phaseOrderIndex) || 0
  for (const s of sections || []) {
    const L = s?.planLoc
    if (L?.phaseKind === 'parallel' && (L.phaseOrderIndex ?? 0) === po) {
      set.add(L.parallelStreamOrderIndex ?? 0)
    }
  }
  return [...set].sort((a, b) => a - b)
}

/** Globale Abschnitts-Indizes eines Streams. */
export function sectionIndicesForParallelStream(sections, phaseOrderIndex, streamOrderIndex) {
  const out = []
  const po = Number(phaseOrderIndex) || 0
  const so = Number(streamOrderIndex) || 0
  ;(sections || []).forEach((s, i) => {
    const L = s?.planLoc
    if (L?.phaseKind === 'parallel' && (L.phaseOrderIndex ?? 0) === po && (L.parallelStreamOrderIndex ?? 0) === so) {
      out.push(i)
    }
  })
  return out
}

/** Abschnitte an den angegebenen globalen Indizes entfernen (mindestens ein Abschnitt bleibt). */
export function reorderWithoutIndices(prev, removeGlobalIndices) {
  const set = new Set(removeGlobalIndices || [])
  const next = (prev || []).filter((_, i) => !set.has(i))
  return next.length ? next : [defaultSection()]
}

/**
 * Ob in den Abschnitten eines Stream-Buckets planerisch etwas steht (Übungen, Text-Anmerkungen).
 * Trennlinien-Marker (---) zählen nicht als Inhalt.
 */
export function parallelStreamBucketHasContent(sections, globalIndices, separatorBody = '---') {
  for (const gi of globalIndices || []) {
    const sec = (sections || [])[gi]
    if (!sec) continue
    for (const it of sec.items || []) {
      if ((it.item_type || '') === 'note') {
        const b = (it.note_body || '').trim()
        if (b && b !== separatorBody) return true
      } else {
        if (it.exercise_id) return true
        if ((it.exercise_title || '').trim()) return true
      }
    }
  }
  return false
}

/** Parallele Phase auflösen: alle Abschnitte dieser Phase werden Ganzgruppe (gleicher phaseOrderIndex). */
export function dissolveParallelPhaseToWholeGroup(sections, phaseOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  return (sections || []).map((s) => {
    const L = s?.planLoc
    if (L?.phaseKind !== 'parallel' || (L.phaseOrderIndex ?? 0) !== po) return s
    return {
      ...s,
      planLoc: {
        ...defaultPlanLocWholeGroup(po),
        phaseTitle: L.phaseTitle ?? null,
        phaseGuidanceNotes: L.phaseGuidanceNotes ?? null,
      },
    }
  })
}

export function reorderWithinBucketIndices(prev, bucketGlobalIndicesSorted, oldPos, newPos) {
  const sortedIdx = [...bucketGlobalIndicesSorted].sort((a, b) => a - b)
  if (oldPos === newPos || oldPos < 0 || newPos < 0 || oldPos >= sortedIdx.length || newPos >= sortedIdx.length) {
    return prev
  }
  const values = sortedIdx.map((gi) => prev[gi])
  const arr = [...values]
  const [x] = arr.splice(oldPos, 1)
  arr.splice(newPos, 0, x)
  const next = [...prev]
  sortedIdx.forEach((gi, k) => {
    next[gi] = arr[k]
  })
  return next
}

function normalizeCatalogMethodProfile(cp) {
  if (cp && typeof cp === 'object' && !Array.isArray(cp)) return { ...cp }
  return {}
}

/** NULL = Planung folgt Katalogprofil der Übung (Reihenfolge beibehalten: zuerst String-JSON auflösen). */
function normalizePlanningMethodProfile(pm) {
  if (pm == null) return null
  if (typeof pm === 'string') {
    const t = pm.trim()
    if (!t || t === 'null') return null
    try {
      const p = JSON.parse(t)
      if (p && typeof p === 'object' && !Array.isArray(p)) return { ...p }
      return null
    } catch {
      return null
    }
  }
  if (typeof pm === 'object' && !Array.isArray(pm)) return { ...pm }
  return null
}

/** Reines JSON für PUT /training-units (vermeidet nicht serialisierbare Werte → 500). */
export function cloneJsonSerializablePlanningProfile(obj) {
  if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) return null
  try {
    return JSON.parse(JSON.stringify(obj))
  } catch {
    return {}
  }
}

export function exerciseRow() {
  return {
    item_type: 'exercise',
    exercise_id: '',
    exercise_variant_id: '',
    exercise_kind: 'simple',
    exercise_title: '',
    variants: [],
    planned_duration_min: '',
    actual_duration_min: '',
    notes: '',
    modifications: '',
    source_training_module_id: '',
    source_module_title: '',
    catalog_method_archetype: '',
    catalog_method_profile: {},
    planning_method_profile: null,
  }
}

export async function hydrateExercisePlanningRow(exercise) {
  let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
  let title = exercise?.title || ''
  let exerciseKind = exercise?.exercise_kind
  const id = exercise?.id
  if (!id) return null
  let meta = {}

  let full

  async function ensureFull() {
    if (full !== undefined) return full
    try {
      full = await api.getExercise(id)
    } catch {
      full = null
    }
    return full
  }

  if (!variants.length) {
    await ensureFull()
    if (full) {
      variants = Array.isArray(full.variants) ? full.variants : []
      title = full.title || title
      if (exerciseKind == null) exerciseKind = full.exercise_kind
      meta = {
        exercise_visibility: full.visibility || 'private',
        exercise_club_id: full.club_id ?? null,
        exercise_created_by: full.created_by ?? null,
        exercise_status: full.status || 'draft',
        catalog_method_archetype: typeof full.method_archetype === 'string' ? full.method_archetype.trim() : '',
        catalog_method_profile: normalizeCatalogMethodProfile(full.method_profile),
      }
    }
  } else {
    meta = {
      exercise_visibility: exercise?.visibility ?? null,
      exercise_club_id: exercise?.club_id ?? null,
      exercise_created_by: exercise?.created_by ?? null,
      exercise_status: exercise?.status ?? null,
    }
    if (
      meta.exercise_visibility == null ||
      meta.exercise_created_by == null ||
      exerciseKind == null
    ) {
      await ensureFull()
      if (full) {
        if (meta.exercise_visibility == null) meta.exercise_visibility = full.visibility || 'private'
        if (meta.exercise_club_id == null) meta.exercise_club_id = full.club_id ?? null
        if (meta.exercise_created_by == null) meta.exercise_created_by = full.created_by ?? null
        if (meta.exercise_status == null) meta.exercise_status = full.status || 'draft'
        if (exerciseKind == null) exerciseKind = full.exercise_kind
        if (!variants.length) variants = Array.isArray(full.variants) ? full.variants : []
      }
    }
    meta.exercise_visibility = meta.exercise_visibility || 'private'
    meta.exercise_status = meta.exercise_status || 'draft'
  }

  const row = exerciseRow()
  row.exercise_id = id
  row.exercise_variant_id = ''
  row.exercise_title = title
  row.exercise_kind =
    String(exerciseKind || 'simple').toLowerCase().trim() === 'combination' ? 'combination' : 'simple'
  if (row.exercise_kind === 'combination') {
    row.variants = []
    row.exercise_variant_id = ''
  } else {
    row.variants = variants
  }
  Object.assign(row, meta)
  if (row.exercise_kind === 'combination') {
    if (full === undefined) await ensureFull()
    if (full) {
      row.catalog_method_archetype =
        typeof full.method_archetype === 'string' ? full.method_archetype.trim() : ''
      row.catalog_method_profile = normalizeCatalogMethodProfile(full.method_profile)
      row.combination_slots = Array.isArray(full.combination_slots) ? full.combination_slots : []
    }
  }
  row.planning_method_profile = null
  return row
}

export function noteRow() {
  return { item_type: 'note', note_body: '', source_training_module_id: '', source_module_title: '' }
}

/** Zur Serialisierung in die Planungs-API (persistente Modul-Herkunft). */
function parseOptionalSourceTrainingModuleIdForPayload(v) {
  if (v === null || v === undefined || v === '') return null
  const n = typeof v === 'number' ? v : parseInt(String(v).trim(), 10)
  return Number.isFinite(n) && n >= 1 ? n : null
}

function sortByOrderIndex(arr) {
  return [...(arr || [])].sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
}

/** Katalog-Abschnitt (GET) → Editor-Zeilen inkl. Kombi/Modul-Meta — wird von Legacy `sections` und von `phases` genutzt. */
function formItemsFromApiItems(items) {
  return (items || []).map((it) => {
    if (it.item_type === 'note') {
      const sm = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id)
      const rowNote = {
        item_type: 'note',
        note_body: it.note_body || '',
        source_training_module_id: '',
        source_module_title: '',
      }
      if (sm != null) {
        rowNote.source_training_module_id = sm
        rowNote.source_module_title = (
          it.source_module_title ||
          it.source_training_module_title ||
          ''
        ).trim()
      }
      return rowNote
    }
    const smEx = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id)
    const ek = String(it.exercise_kind || 'simple').toLowerCase().trim()
    const isCombo = ek === 'combination'
    return {
      item_type: 'exercise',
      exercise_id: it.exercise_id,
      exercise_kind: isCombo ? 'combination' : 'simple',
      exercise_variant_id: isCombo ? '' : it.exercise_variant_id ?? '',
      exercise_title: it.exercise_title || '',
      variants: [],
      planned_duration_min:
        it.planned_duration_min !== null && it.planned_duration_min !== undefined
          ? String(it.planned_duration_min)
          : '',
      actual_duration_min:
        it.actual_duration_min !== null && it.actual_duration_min !== undefined
          ? String(it.actual_duration_min)
          : '',
      notes: it.notes ?? '',
      modifications: it.modifications ?? '',
      catalog_method_archetype: String(it.catalog_method_archetype ?? '').trim(),
      catalog_method_profile: normalizeCatalogMethodProfile(it.catalog_method_profile),
      planning_method_profile: normalizePlanningMethodProfile(it.planning_method_profile),
      ...(smEx != null
        ? {
            source_training_module_id: smEx,
            source_module_title: (
              it.source_module_title ||
              it.source_training_module_title ||
              ''
            ).trim(),
          }
        : {}),
    }
  })
}

/** GET `phases` → flache Editor-Abschnitte mit `planLoc` (für PUT `phases` Roundtrip). */
function normalizePhasesToFormSections(fullUnit) {
  const phases = sortByOrderIndex(fullUnit.phases || [])
  const out = []
  for (const ph of phases) {
    const pOi = ph.order_index ?? 0
    const pk = String(ph.phase_kind || 'whole_group').toLowerCase().trim()
    const basePhaseLoc = {
      phaseTitle: ph.title ?? null,
      phaseGuidanceNotes: ph.guidance_notes ?? null,
    }
    if (pk === 'parallel') {
      for (const st of sortByOrderIndex(ph.streams || [])) {
        const sOi = st.order_index ?? 0
        const streamLoc = {
          phaseKind: 'parallel',
          phaseOrderIndex: pOi,
          parallelStreamOrderIndex: sOi,
          ...basePhaseLoc,
          streamTitle: st.title ?? null,
          streamNotes: st.notes ?? null,
          streamAssignedTrainerProfileIds: st.assigned_trainer_profile_ids ?? null,
        }
        for (const sec of sortByOrderIndex(st.sections || [])) {
          out.push({
            title: sec.title,
            guidance_notes: sec.guidance_notes || '',
            items: formItemsFromApiItems(sec.items),
            planLoc: { ...streamLoc },
          })
        }
      }
    } else {
      const loc = {
        phaseKind: 'whole_group',
        phaseOrderIndex: pOi,
        parallelStreamOrderIndex: null,
        ...basePhaseLoc,
        streamTitle: null,
        streamNotes: null,
        streamAssignedTrainerProfileIds: null,
      }
      for (const sec of sortByOrderIndex(ph.sections || [])) {
        out.push({
          title: sec.title,
          guidance_notes: sec.guidance_notes || '',
          items: formItemsFromApiItems(sec.items),
          planLoc: { ...loc },
        })
      }
    }
  }
  return out.length ? out : [defaultSection()]
}

export function normalizeUnitToForm(fullUnit) {
  if (Array.isArray(fullUnit?.phases) && fullUnit.phases.length > 0) {
    return normalizePhasesToFormSections(fullUnit)
  }
  if (fullUnit.sections && fullUnit.sections.length) {
    return fullUnit.sections.map((sec) => ({
      title: sec.title,
      guidance_notes: sec.guidance_notes || '',
      items: formItemsFromApiItems(sec.items),
    }))
  }
  if (fullUnit.exercises && fullUnit.exercises.length) {
    return [
      {
        title: 'Übungen',
        guidance_notes: '',
        items: fullUnit.exercises.map((ex) => {
          const ek = String(ex.exercise_kind || 'simple').toLowerCase().trim()
          const isCombo = ek === 'combination'
          return {
            item_type: 'exercise',
            exercise_kind: ek,
            exercise_id: ex.exercise_id,
            exercise_variant_id: isCombo ? '' : (ex.exercise_variant_id ?? ''),
            exercise_title: ex.exercise_title || '',
            variants: [],
            planned_duration_min:
              ex.planned_duration_min !== null && ex.planned_duration_min !== undefined
                ? String(ex.planned_duration_min)
                : '',
            actual_duration_min:
              ex.actual_duration_min !== null && ex.actual_duration_min !== undefined
                ? String(ex.actual_duration_min)
                : '',
            notes: ex.notes ?? '',
            modifications: ex.modifications ?? '',
            catalog_method_archetype: String(ex.catalog_method_archetype ?? '').trim(),
            catalog_method_profile: normalizeCatalogMethodProfile(ex.catalog_method_profile),
            planning_method_profile: normalizePlanningMethodProfile(ex.planning_method_profile),
          }
        }),
      },
    ]
  }
  return [defaultSection()]
}

export async function enrichSectionsWithVariants(sections) {
  if (!sections?.length) return sections
  const ids = []
  for (const sec of sections) {
    for (const it of sec.items || []) {
      if (it.item_type === 'note') continue
      if (it.exercise_id) ids.push(it.exercise_id)
    }
  }
  const unique = [...new Set(ids)]
  const cache = new Map()
  await Promise.all(
    unique.map(async (id) => {
      try {
        const ex = await api.getExercise(id)
        const ek = String(ex.exercise_kind || 'simple').toLowerCase().trim()
        cache.set(id, {
          title: ex.title || '',
          exercise_kind: ek,
          variants: Array.isArray(ex.variants) ? ex.variants : [],
          visibility: ex.visibility || 'private',
          club_id: ex.club_id ?? null,
          created_by: ex.created_by ?? null,
          status: ex.status || 'draft',
          method_archetype: typeof ex.method_archetype === 'string' ? ex.method_archetype.trim() : '',
          method_profile: normalizeCatalogMethodProfile(ex.method_profile),
          combination_slots: ek === 'combination' && Array.isArray(ex.combination_slots) ? ex.combination_slots : [],
        })
      } catch {
        cache.set(id, {
          title: '',
          exercise_kind: 'simple',
          variants: [],
          visibility: 'private',
          club_id: null,
          created_by: null,
          status: 'draft',
          method_archetype: '',
          method_profile: {},
          combination_slots: [],
        })
      }
    })
  )

  const titleById = new Map()
  for (const id of unique) {
    const row = cache.get(id)
    const t = (row?.title || '').trim()
    if (t) titleById.set(Number(id), t)
  }
  const comboCandidateExtra = new Set()
  for (const id of unique) {
    const row = cache.get(id)
    if (String(row?.exercise_kind || '').toLowerCase().trim() !== 'combination') continue
    for (const slot of row.combination_slots || []) {
      for (const raw of slot.candidate_exercise_ids || []) {
        const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
        if (Number.isFinite(n) && !titleById.has(n)) comboCandidateExtra.add(n)
      }
    }
  }
  await Promise.all(
    [...comboCandidateExtra].map(async (cid) => {
      try {
        const ex = await api.getExercise(cid)
        titleById.set(cid, ((ex.title || '').trim() || `Übung #${cid}`))
      } catch {
        titleById.set(cid, `Übung #${cid}`)
      }
    }),
  )

  function comboMemberTitleByIdForSlots(slots) {
    const o = {}
    for (const slot of slots || []) {
      for (const raw of slot.candidate_exercise_ids || []) {
        const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
        if (!Number.isFinite(n)) continue
        const key = String(n)
        if (!o[key]) o[key] = titleById.get(n) || `Übung #${n}`
      }
    }
    return o
  }

  return sections.map((sec) => ({
    ...sec,
    items: (sec.items || []).map((it) => {
      if (it.item_type === 'note') return it
      if (!it.exercise_id) return it
      const c = cache.get(it.exercise_id)
      if (!c) return it
      const ek = String(c.exercise_kind || 'simple').toLowerCase().trim()
      const isCombo = ek === 'combination'
      const itemCatalog = normalizeCatalogMethodProfile(it.catalog_method_profile)
      const catalog_method_profile =
        Object.keys(itemCatalog).length > 0
          ? itemCatalog
          : normalizeCatalogMethodProfile(c.method_profile)
      const rowArche = String(it.catalog_method_archetype ?? '').trim()
      const catalog_method_archetype = rowArche || String(c.method_archetype ?? '').trim()
      return {
        ...it,
        catalog_method_archetype,
        catalog_method_profile,
        planning_method_profile: normalizePlanningMethodProfile(it.planning_method_profile),
        exercise_kind: isCombo ? 'combination' : 'simple',
        exercise_title: it.exercise_title || c.title,
        exercise_variant_id: isCombo ? '' : it.exercise_variant_id,
        variants:
          isCombo ? [] : Array.isArray(it.variants) && it.variants.length > 0 ? it.variants : c.variants,
        exercise_visibility: c.visibility,
        exercise_club_id: c.club_id,
        exercise_created_by: c.created_by,
        exercise_status: c.status,
        ...(isCombo ? { combination_slots: c.combination_slots || [], combo_member_title_by_id: comboMemberTitleByIdForSlots(c.combination_slots || []) } : {}),
      }
    }),
  }))
}

/**
 * Outline für CombinationMethodProfileEditor: pro‑Slot‑Zeiten nur sichtbar, wenn Stationen übergeben werden.
 */
export function comboSlotsOutlineForProfileEditor(combinationSlots) {
  if (!Array.isArray(combinationSlots) || combinationSlots.length === 0) return null
  const sorted = sortCombinationSlotsForDisplay(combinationSlots)
  return sorted.map((s, i) => {
    const rawIx = s.slot_index
    const si =
      rawIx === '' || rawIx == null ? null : typeof rawIx === 'number' ? rawIx : parseInt(String(rawIx), 10)
    return {
      slot_index: Number.isFinite(si) ? si : i,
      title: (s.title != null ? String(s.title) : '').trim(),
    }
  })
}

export function parseMin(v) {
  if (v === '' || v === null || v === undefined) return null
  const n = parseInt(String(v), 10)
  return Number.isFinite(n) ? n : null
}

export function buildOneSectionPayload(sec, orderIndex) {
  return {
    order_index: orderIndex,
    title: (sec.title || '').trim() || 'Abschnitt',
    guidance_notes: sec.guidance_notes?.trim() ? sec.guidance_notes.trim() : null,
    items: (sec.items || [])
      .map((it, ii) => {
        if (it.item_type === 'note') {
          const sm = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id)
          const row = {
            item_type: 'note',
            order_index: ii,
            note_body: it.note_body ?? '',
          }
          if (sm != null) row.source_training_module_id = sm
          return row
        }
        if (it.exercise_id === '' || it.exercise_id == null || Number.isNaN(Number(it.exercise_id))) {
          return null
        }
        const isCombo = String(it.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
        const vid = isCombo ? null : it.exercise_variant_id
        const smEx = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id)
        const rowEx = {
          item_type: 'exercise',
          order_index: ii,
          exercise_id: parseInt(it.exercise_id, 10),
          exercise_variant_id:
            vid !== '' && vid != null && !Number.isNaN(Number(vid)) ? parseInt(vid, 10) : null,
          planned_duration_min: parseMin(it.planned_duration_min),
          actual_duration_min: parseMin(it.actual_duration_min),
          notes: it.notes?.trim() ? it.notes.trim() : null,
          modifications: it.modifications?.trim() ? it.modifications.trim() : null,
        }
        if (isCombo) {
          const pmp = it.planning_method_profile
          if (pmp != null && typeof pmp === 'object' && !Array.isArray(pmp)) {
            const cleaned = cloneJsonSerializablePlanningProfile(pmp)
            if (cleaned && Object.keys(cleaned).length > 0) {
              rowEx.planning_method_profile = cleaned
            }
          }
        }
        if (smEx != null) rowEx.source_training_module_id = smEx
        return rowEx
      })
      .filter(Boolean),
  }
}

/** PUT /api/training-units: Legacy `sections` (eine whole_group-Phase) wenn kein `planLoc` gesetzt ist. */
export function buildSectionsPayload(sections) {
  return sections.map((sec, si) => buildOneSectionPayload(sec, si))
}

function stripPlanLoc(sec) {
  if (!sec || typeof sec !== 'object') return sec
  const { planLoc: _pl, ...rest } = sec
  return rest
}

function phaseIndexToInt(v, fallback = 0) {
  if (v === null || v === undefined || v === '') return fallback
  const n = typeof v === 'number' ? v : Number(v)
  return Number.isFinite(n) ? n : fallback
}

/**
 * planLoc für Phasen-PUT kanonisieren (Großschreibung, numerische Indizes).
 * Verhindert u. a. abgebrochene Phasen-Runs bei "0" !== 0 und falsche whole_group-Zweige.
 */
function canonicalPlanLocForPhasedSave(pl) {
  if (!pl || typeof pl !== 'object') return null
  const rawKind = String(pl.phaseKind || '').toLowerCase().trim()
  let kind = rawKind === 'parallel' || rawKind === 'whole_group' ? rawKind : null
  if (
    !kind &&
    pl.parallelStreamOrderIndex != null &&
    pl.parallelStreamOrderIndex !== ''
  ) {
    kind = 'parallel'
  }
  if (!kind) return null
  const phaseOrderIndex = phaseIndexToInt(pl.phaseOrderIndex, 0)
  let parallelStreamOrderIndex = null
  if (kind === 'parallel') {
    parallelStreamOrderIndex = phaseIndexToInt(pl.parallelStreamOrderIndex, 0)
  }
  return {
    ...pl,
    phaseKind: kind,
    phaseOrderIndex,
    parallelStreamOrderIndex,
  }
}

export function inheritPlanLocForPhasedSave(sections) {
  let prev = { ...defaultPlanLocWholeGroup(0) }
  return (sections || []).map((s) => {
    const canon = canonicalPlanLocForPhasedSave(s?.planLoc)
    if (canon) {
      prev = { ...canon }
      return { ...s, planLoc: prev }
    }
    return { ...s, planLoc: { ...prev } }
  })
}

/** Phasen-„Runs“ in der flachen Abschnittsliste (Reihenfolge wie beim Speichern). */
export function phaseRunsFromSections(sections) {
  const norm = inheritPlanLocForPhasedSave(sections)
  const runs = []
  let i = 0
  while (i < norm.length) {
    const loc0 = norm[i]?.planLoc
    if (!loc0?.phaseKind) {
      i += 1
      continue
    }
    const pOi = loc0.phaseOrderIndex ?? 0
    const pk = loc0.phaseKind === 'parallel' ? 'parallel' : 'whole_group'
    const start = i
    while (i < norm.length) {
      const L = norm[i]?.planLoc
      if (!L?.phaseKind) break
      const pk2 = L.phaseKind === 'parallel' ? 'parallel' : 'whole_group'
      if ((L.phaseOrderIndex ?? 0) !== pOi || pk2 !== pk) break
      i += 1
    }
    runs.push({ phaseKind: pk, phaseOrderIndex: pOi, start, end: i })
  }
  return runs
}

/** Vertauscht zwei unmittelbar benachbarte Runs (upperRunIndex = erste der beiden). */
export function swapAdjacentPhaseRuns(prev, upperRunIndex) {
  const runs = phaseRunsFromSections(prev)
  const a = upperRunIndex
  const b = upperRunIndex + 1
  if (a < 0 || b >= runs.length) return prev
  const rgA = runs[a]
  const rgB = runs[b]
  const head = prev.slice(0, rgA.start)
  const blA = prev.slice(rgA.start, rgA.end)
  const blB = prev.slice(rgB.start, rgB.end)
  const tail = prev.slice(rgB.end)
  return [...head, ...blB, ...blA, ...tail]
}

export function movePhaseRunUpByPhaseOrder(prev, phaseOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const runs = phaseRunsFromSections(prev)
  const rIdx = runs.findIndex((r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po)
  if (rIdx <= 0) return prev
  return swapAdjacentPhaseRuns(prev, rIdx - 1)
}

export function movePhaseRunDownByPhaseOrder(prev, phaseOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const runs = phaseRunsFromSections(prev)
  const rIdx = runs.findIndex((r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po)
  if (rIdx < 0 || rIdx >= runs.length - 1) return prev
  return swapAdjacentPhaseRuns(prev, rIdx)
}

/**
 * Abschnitt verschieben und planLoc an der Einfügestelle an Nachbarn anpassen.
 * Regel: Einfügen vor Abschnitt X übernimmt X.planLoc; am Listenende nach Parallel → neue Ganzgruppen-Phase.
 */
export function reorderBlocksImmutableWithPlanLoc(prev, fromI, toBeforeIdx) {
  const arr = [...prev]
  if (fromI < 0 || fromI >= arr.length) return prev
  const [moved] = arr.splice(fromI, 1)
  let insertAt = toBeforeIdx
  if (fromI < toBeforeIdx) insertAt = toBeforeIdx - 1
  insertAt = Math.max(0, Math.min(insertAt, arr.length))

  const below = insertAt < arr.length ? arr[insertAt] : undefined
  const above = insertAt > 0 ? arr[insertAt - 1] : undefined

  let planLocNext = null
  const belowKind = below?.planLoc?.phaseKind
  const aboveKind = above?.planLoc?.phaseKind
  const movedKind = moved?.planLoc?.phaseKind
  const belowPo = below?.planLoc?.phaseOrderIndex ?? 0
  const movedPo = moved?.planLoc?.phaseOrderIndex ?? 0

  if (belowKind === 'parallel' && aboveKind === 'whole_group') {
    if (movedKind !== 'parallel') {
      planLocNext = { ...above.planLoc }
    } else if (movedPo !== belowPo) {
      planLocNext = { ...below.planLoc }
    }
  } else if (belowKind === 'parallel' && !above) {
    if (movedKind !== 'parallel') {
      planLocNext = defaultPlanLocWholeGroup(0)
    }
  }

  if (!planLocNext && belowKind) {
    planLocNext = { ...below.planLoc }
  }
  if (!planLocNext && insertAt === arr.length) {
    if (!above) {
      planLocNext = defaultPlanLocWholeGroup(0)
    } else if (above.planLoc?.phaseKind === 'parallel') {
      const mx = maxPhaseOrderIndexFromSections(arr)
      planLocNext = defaultPlanLocWholeGroup(mx + 1)
    } else if (above.planLoc?.phaseKind === 'whole_group') {
      planLocNext = { ...above.planLoc }
    }
  }
  if (!planLocNext && above?.planLoc?.phaseKind === 'whole_group') {
    planLocNext = { ...above.planLoc }
  }
  if (!planLocNext && above?.planLoc?.phaseKind === 'parallel') {
    const mx = maxPhaseOrderIndexFromSections(arr)
    planLocNext = defaultPlanLocWholeGroup(mx + 1)
  }

  let nextMoved = { ...moved }
  if (planLocNext) {
    nextMoved = { ...moved, planLoc: planLocNext }
  } else {
    nextMoved = stripPlanLoc(moved)
  }
  arr.splice(insertAt, 0, nextMoved)
  return arr
}

/**
 * Abschnitt direkt vor den Parallel-Lauf setzen (immer Ganzgruppe oberhalb der Split-Phase).
 */
export function reorderSectionBeforeParallelRunAsWholeGroup(prev, fromI, phaseOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const idxs = indicesOfParallelPhase(prev, po)
  if (!idxs.length || fromI < 0 || fromI >= (prev?.length ?? 0)) return prev
  const fg = idxs[0]
  const arr = [...prev]
  const [moved] = arr.splice(fromI, 1)
  let insertAt = fg
  if (fromI < insertAt) insertAt -= 1
  insertAt = Math.max(0, Math.min(insertAt, arr.length))
  const above = insertAt > 0 ? arr[insertAt - 1] : undefined
  let planLocNext
  if (above?.planLoc?.phaseKind === 'whole_group') {
    planLocNext = { ...above.planLoc }
  } else if (!above) {
    planLocNext = defaultPlanLocWholeGroup(0)
  } else {
    const mx = maxPhaseOrderIndexFromSections(arr)
    planLocNext = defaultPlanLocWholeGroup(mx + 1)
  }
  arr.splice(insertAt, 0, { ...moved, planLoc: planLocNext })
  return arr
}

/** Abschnitt als neuen ersten Eintrag der Parallel-Phase (gleiche Phasen-/Stream-Metadaten wie bisheriger Kopf). */
export function reorderSectionAsFirstInParallelPhase(prev, fromI, phaseOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const idxs = indicesOfParallelPhase(prev, po)
  if (!idxs.length || fromI < 0 || fromI >= (prev?.length ?? 0)) return prev
  let fg = idxs[0]
  const arr = [...prev]
  const headTpl = { ...arr[fg].planLoc }
  const [moved] = arr.splice(fromI, 1)
  if (fromI < fg) fg -= 1
  fg = Math.max(0, Math.min(fg, arr.length))
  arr.splice(fg, 0, { ...moved, planLoc: { ...headTpl } })
  return arr
}

/** Abschnitt als ersten Eintrag eines parallelen Streams setzen (planLoc wie erster Abschnitt dieses Streams, bzw. leerer Stream wie reorderBlockIntoParallelStreamEnd). */
export function reorderSectionAsFirstInParallelStream(prev, fromI, phaseOrderIndex, streamOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const so = Number(streamOrderIndex) || 0
  const len = prev?.length ?? 0
  if (fromI < 0 || fromI >= len) return prev

  const arr = [...prev]
  const [moved] = arr.splice(fromI, 1)

  const streamIdx = sectionIndicesForParallelStream(arr, po, so)
  let insertAt
  let headTpl
  let skipFromIAdjust = false

  if (streamIdx.length) {
    const first = Math.min(...streamIdx)
    headTpl = { ...arr[first].planLoc }
    insertAt = first
  } else {
    const phaseIdx = indicesOfParallelPhase(arr, po)
    if (!phaseIdx.length) {
      const ml = moved?.planLoc
      if (ml?.phaseKind !== 'parallel' || (ml.phaseOrderIndex ?? 0) !== po) return prev
      headTpl = {
        ...ml,
        parallelStreamOrderIndex: so,
        streamTitle: null,
        streamNotes: null,
        streamAssignedTrainerProfileIds: null,
      }
      insertAt = Math.min(fromI, arr.length)
      skipFromIAdjust = true
    } else {
      const ref = arr[phaseIdx[phaseIdx.length - 1]]
      headTpl = {
        ...ref.planLoc,
        parallelStreamOrderIndex: so,
        streamTitle: null,
        streamNotes: null,
        streamAssignedTrainerProfileIds: null,
      }
      insertAt = phaseIdx[phaseIdx.length - 1] + 1
    }
  }

  if (!skipFromIAdjust && fromI < insertAt) insertAt -= 1
  insertAt = Math.max(0, Math.min(insertAt, arr.length))
  arr.splice(insertAt, 0, { ...moved, planLoc: { ...headTpl } })
  return arr
}

/**
 * Abschnitt ans Ende eines parallelen Streams setzen (planLoc wie dieser Stream).
 * Leerer Stream: Einfügen hinter den letzten Abschnitt der zugehörigen parallelen Phase, planLoc vom Referenz-Abschnitt mit angepasstem streamIndex.
 */
export function reorderBlockIntoParallelStreamEnd(prev, fromI, phaseOrderIndex, streamOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const so = Number(streamOrderIndex) || 0
  const len = prev?.length ?? 0
  if (fromI < 0 || fromI >= len) return prev
  const arr = [...prev]
  const [moved] = arr.splice(fromI, 1)
  const streamIdx = sectionIndicesForParallelStream(arr, po, so)
  let insertAt
  let planLocTemplate

  if (streamIdx.length) {
    const last = Math.max(...streamIdx)
    planLocTemplate = { ...arr[last].planLoc }
    insertAt = last + 1
  } else {
    const phaseIdx = indicesOfParallelPhase(arr, po)
    if (!phaseIdx.length) return prev
    const ref = arr[phaseIdx[phaseIdx.length - 1]]
    planLocTemplate = {
      ...ref.planLoc,
      parallelStreamOrderIndex: so,
      streamTitle: null,
      streamNotes: null,
      streamAssignedTrainerProfileIds: null,
    }
    insertAt = phaseIdx[phaseIdx.length - 1] + 1
  }

  insertAt = Math.max(0, Math.min(insertAt, arr.length))
  arr.splice(insertAt, 0, { ...moved, planLoc: { ...planLocTemplate } })
  return arr
}

/** Globales insertBeforeIndex, um einen Abschnitt hinter den letzten des Streams einzufügen (z. B. Rahmen-Slots). */
export function globalInsertBeforeIndexForParallelStreamEnd(sections, phaseOrderIndex, streamOrderIndex) {
  const arr = sections || []
  const po = Number(phaseOrderIndex) || 0
  const so = Number(streamOrderIndex) || 0
  const streamIdx = sectionIndicesForParallelStream(arr, po, so)
  if (streamIdx.length) return Math.max(...streamIdx) + 1
  const phaseIdx = indicesOfParallelPhase(arr, po)
  if (!phaseIdx.length) return arr.length
  return Math.max(...phaseIdx) + 1
}

/** Alle globalen Indizes einer parallelen Phase (alle Streams), sortiert. */
export function indicesOfParallelPhase(sections, phaseOrderIndex) {
  const po = Number(phaseOrderIndex) || 0
  const out = []
  ;(sections || []).forEach((s, i) => {
    const L = s?.planLoc
    if (L?.phaseKind === 'parallel' && (L.phaseOrderIndex ?? 0) === po) out.push(i)
  })
  return out.sort((a, b) => a - b)
}

/** Gesamten Parallel-Block an neue Position (insertBefore globale Liste) schieben. */
export function moveParallelPhaseRunToInsertBefore(prev, phaseOrderIndex, toBeforeIdx) {
  const po = Number(phaseOrderIndex) || 0
  const indices = indicesOfParallelPhase(prev, po)
  if (!indices.length) return prev
  const indexSet = new Set(indices)
  const blocks = indices.map((i) => prev[i])
  const without = prev.filter((_, i) => !indexSet.has(i))
  let ins = toBeforeIdx
  for (const i of indices) {
    if (i < toBeforeIdx) ins -= 1
  }
  ins = Math.max(0, Math.min(ins, without.length))
  return [...without.slice(0, ins), ...blocks, ...without.slice(ins)]
}

/**
 * Nach Drag&Drop: wenn aus einer Parallelphase noch ≤1 Stream übrig ist (vorher ≥2), Rückfrage wie beim Stream-Löschen.
 */
export function afterSectionReorderParallelGuard(prev, next) {
  const seenPo = new Set()
  for (const s of prev || []) {
    const L = s?.planLoc
    if (L?.phaseKind !== 'parallel') continue
    seenPo.add(L.phaseOrderIndex ?? 0)
  }
  let out = next
  for (const po of seenPo) {
    const prevN = streamsForParallelPhaseOrders(prev, po).length
    if (prevN < 2) continue
    const nowN = streamsForParallelPhaseOrders(out, po).length
    if (nowN <= 1) {
      if (
        window.confirm(
          'In dieser parallelen Phase ist nur noch eine Gruppe übrig. Parallelaufbau auflösen und alle zugehörigen Abschnitte als gemeinsame Ganzgruppen-Phase führen?'
        )
      ) {
        out = dissolveParallelPhaseToWholeGroup(out, po)
      }
    }
  }
  return out
}

function buildPhasesPayloadFromFlat(sections) {
  const norm = inheritPlanLocForPhasedSave(sections)
  const phases = []
  let i = 0
  while (i < norm.length) {
    const loc0 = norm[i].planLoc
    const pOi = loc0.phaseOrderIndex ?? 0
    const pk = loc0.phaseKind === 'parallel' ? 'parallel' : 'whole_group'

    const run = []
    while (i < norm.length) {
      const L = norm[i].planLoc
      const pk2 = L.phaseKind === 'parallel' ? 'parallel' : 'whole_group'
      if ((L.phaseOrderIndex ?? 0) !== pOi || pk2 !== pk) break
      run.push(norm[i])
      i += 1
    }

    const head = run[0].planLoc
    if (pk === 'whole_group') {
      phases.push({
        phase_kind: 'whole_group',
        order_index: pOi,
        title: head.phaseTitle ?? null,
        guidance_notes: head.phaseGuidanceNotes ?? null,
        sections: run.map((s, idx) => buildOneSectionPayload(stripPlanLoc(s), idx)),
      })
    } else {
      const byStream = new Map()
      for (const s of run) {
        const soi = s.planLoc.parallelStreamOrderIndex ?? 0
        if (!byStream.has(soi)) byStream.set(soi, [])
        byStream.get(soi).push(s)
      }
      const streamOrder = [...byStream.keys()].sort((a, b) => a - b)
      const streams = streamOrder.map((soi) => {
        const bucket = byStream.get(soi)
        const h = bucket[0].planLoc
        const st = {
          order_index: soi,
          title: h.streamTitle ?? null,
          notes: h.streamNotes ?? null,
          sections: bucket.map((s, idx) => buildOneSectionPayload(stripPlanLoc(s), idx)),
        }
        const asst = h.streamAssignedTrainerProfileIds
        if (asst !== null && asst !== undefined) st.assigned_trainer_profile_ids = asst
        return st
      })
      phases.push({
        phase_kind: 'parallel',
        order_index: pOi,
        title: head.phaseTitle ?? null,
        guidance_notes: head.phaseGuidanceNotes ?? null,
        streams,
      })
    }
  }
  return { phases }
}

/**
 * Speichern einer Einheit: flache `sections` oder verschachtelte `phases`, sobald `planLoc` gesetzt ist.
 */
export function buildPlanPayloadForSave(sections) {
  const list = Array.isArray(sections) ? sections : []
  const anyPhased = list.some((s) => s && canonicalPlanLocForPhasedSave(s.planLoc) != null)
  if (!anyPhased) {
    return { sections: buildSectionsPayload(list) }
  }
  return buildPhasesPayloadFromFlat(list)
}

/** Fügt die Positionen eines Moduls in lokale Abschnitte ein (wie eine Übung, ohne Zwischenspeichern der Einheit). */
export async function insertTrainingModuleIntoPlanningSections({
  sections,
  moduleDetail,
  sectionIndex,
  insertBeforeItemIndex,
}) {
  const secIx = typeof sectionIndex === 'number' ? sectionIndex : parseInt(String(sectionIndex), 10)
  if (
    !Array.isArray(sections) ||
    !Number.isFinite(secIx) ||
    secIx < 0 ||
    secIx >= sections.length ||
    !moduleDetail ||
    typeof moduleDetail !== 'object'
  ) {
    return sections
  }
  const prev = [...(sections[secIx].items || [])]
  let beforeIx
  if (insertBeforeItemIndex === null || insertBeforeItemIndex === undefined || insertBeforeItemIndex === 'end') {
    beforeIx = prev.length
  } else if (insertBeforeItemIndex === 'start') {
    beforeIx = 0
  } else {
    const n = typeof insertBeforeItemIndex === 'number' ? insertBeforeItemIndex : parseInt(String(insertBeforeItemIndex), 10)
    beforeIx = Number.isFinite(n) ? Math.min(Math.max(n, 0), prev.length) : prev.length
  }

  const midRaw = moduleDetail.id
  const midNum = typeof midRaw === 'number' ? midRaw : parseInt(String(midRaw), 10)
  if (!Number.isFinite(midNum) || midNum < 1) return sections

  const modTitle = (moduleDetail.title || '').trim() || `Modul #${midNum}`
  const modItems = [...(moduleDetail.items || [])].sort(
    (a, b) => (a.order_index ?? 0) - (b.order_index ?? 0)
  )

  const appendRows = []
  for (const mi of modItems) {
    if (mi.item_type === 'note') {
      appendRows.push({
        item_type: 'note',
        note_body: mi.note_body || '',
        source_training_module_id: midNum,
        source_module_title: modTitle,
      })
      continue
    }
    if (!mi.exercise_id) continue
    const hydrated = await hydrateExercisePlanningRow({ id: mi.exercise_id })
    if (!hydrated) continue
    hydrated.source_training_module_id = midNum
    hydrated.source_module_title = modTitle
    if (
      hydrated.exercise_kind !== 'combination' &&
      mi.exercise_variant_id
    ) {
      hydrated.exercise_variant_id = String(mi.exercise_variant_id)
    }
    hydrated.planned_duration_min =
      mi.planned_duration_min !== null && mi.planned_duration_min !== undefined
        ? String(mi.planned_duration_min)
        : ''
    hydrated.notes = mi.notes ?? ''
    appendRows.push(hydrated)
  }

  const mergedItems = [...prev.slice(0, beforeIx), ...appendRows, ...prev.slice(beforeIx)]
  return sections.map((sec, idx) => (idx === secIx ? { ...sec, items: mergedItems } : sec))
}

export function sectionPlannedMinutes(sec) {
  return (sec.items || []).reduce((sum, it) => {
    if (it.item_type !== 'exercise') return sum
    const m = parseMin(it.planned_duration_min)
    return sum + (m || 0)
  }, 0)
}
