/**
 * Hilfen für Trainingsplan-Ansicht, Coach-Modus und API-Payload für PUT /training-units/:id.
 */

import {
  buildPlanPayloadForSave,
  cloneJsonSerializablePlanningProfile,
  defaultPlanLocWholeGroup,
  inheritPlanLocForPhasedSave,
  phaseRunsFromSections,
  sectionIndicesForParallelStream,
  streamsForParallelPhaseOrders,
} from './trainingUnitSectionsForm'
export function sortedSections(unit) {
  const raw = unit?.sections
  if (!Array.isArray(raw)) return []
  return [...raw].sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
}

export function sortedItems(sec) {
  const raw = sec?.items
  if (!Array.isArray(raw)) return []
  return [...raw].sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
}

export function itemStableKey(it, secOrder, ix) {
  if (it && it.id != null) return String(it.id)
  return `${secOrder}-${it?.item_type || 'row'}-${ix}`
}

export function sumExerciseMinutesInSection(sec) {
  let t = 0
  for (const it of sortedItems(sec)) {
    if (it.item_type === 'exercise' && it.planned_duration_min != null) {
      const n = Number(it.planned_duration_min)
      if (Number.isFinite(n)) t += n
    }
  }
  return t
}

/**
 * GET liefert `planLoc` oft nicht auf flachen `sections`, aber `unit.phases` (verschachtelt).
 * Baut pro Abschnitt `planLoc` für phaseRuns / Darstellung (camelCase wie im Editor).
 */
function planLocBySectionIdFromPhases(phases) {
  const byId = new Map()
  if (!Array.isArray(phases)) return byId
  for (const ph of phases) {
    const po = Number(ph.order_index ?? ph.orderIndex ?? 0) || 0
    const pk = String(ph.phase_kind ?? ph.phaseKind ?? '')
      .toLowerCase()
      .trim()
    const phaseTitle = ph.title ?? ph.phaseTitle ?? null
    const phaseGuidanceNotes = ph.guidance_notes ?? ph.guidanceNotes ?? null
    if (pk === 'whole_group') {
      for (const sec of ph.sections || []) {
        const sid = sec.id != null ? Number(sec.id) : NaN
        if (!Number.isFinite(sid)) continue
        byId.set(sid, {
          phaseKind: 'whole_group',
          phaseOrderIndex: po,
          parallelStreamOrderIndex: null,
          phaseTitle,
          phaseGuidanceNotes,
          streamTitle: null,
          streamNotes: null,
          streamAssignedTrainerProfileIds: null,
        })
      }
    } else if (pk === 'parallel') {
      for (const st of ph.streams || []) {
        const so = Number(st.order_index ?? st.orderIndex ?? 0) || 0
        const streamTitle = st.title ?? st.streamTitle ?? null
        const streamNotes = st.notes ?? st.streamNotes ?? null
        const streamAssignedTrainerProfileIds =
          st.assigned_trainer_profile_ids ?? st.streamAssignedTrainerProfileIds ?? null
        for (const sec of st.sections || []) {
          const sid = sec.id != null ? Number(sec.id) : NaN
          if (!Number.isFinite(sid)) continue
          byId.set(sid, {
            phaseKind: 'parallel',
            phaseOrderIndex: po,
            parallelStreamOrderIndex: so,
            phaseTitle,
            phaseGuidanceNotes,
            streamTitle,
            streamNotes,
            streamAssignedTrainerProfileIds,
          })
        }
      }
    }
  }
  return byId
}

function maxPhaseOrderIndexFromNestedPhases(phases) {
  let m = -1
  if (!Array.isArray(phases)) return m
  for (const ph of phases) {
    const po = Number(ph.order_index ?? ph.orderIndex ?? 0)
    if (Number.isFinite(po)) m = Math.max(m, po)
  }
  return m
}

export function sectionsWithPlanLocForDisplay(unit) {
  const sorted = sortedSections(unit)
  const byId = planLocBySectionIdFromPhases(unit?.phases)
  const merged = sorted.map((s) => {
    const sid = s.id != null ? Number(s.id) : NaN
    if (Number.isFinite(sid) && byId.has(sid)) {
      return { ...s, planLoc: { ...byId.get(sid) } }
    }
    if (s.planLoc && s.planLoc.phaseKind) return s
    return { ...s }
  })
  const inherited = inheritPlanLocForPhasedSave(merged)
  const maxPhPo = maxPhaseOrderIndexFromNestedPhases(unit?.phases)
  return inherited.map((s) => {
    const sid = s.id != null ? Number(s.id) : NaN
    if (!Number.isFinite(sid) || byId.has(sid)) return s
    const pl = s.planLoc
    if (pl?.phaseKind === 'parallel') {
      const po = maxPhPo >= 0 ? maxPhPo + 1 : 0
      return {
        ...s,
        planLoc: {
          ...defaultPlanLocWholeGroup(po),
          phaseTitle: pl.phaseTitle ?? null,
          phaseGuidanceNotes: pl.phaseGuidanceNotes ?? null,
        },
      }
    }
    return s
  })
}

/**
 * Läuft auf bereits angereichter Abschnittsliste (gleiche Objektreferenzen wie in Slices).
 */
export function buildPlanRunViewModelFromSections(sections) {
  if (!sections.length) {
    return { mode: 'empty', runs: [], totalMin: 0, runCumulativeEnds: [] }
  }

  const runsMeta = phaseRunsFromSections(sections)
  const runs = []
  let cum = 0
  const runCumulativeEnds = []

  if (runsMeta.length === 0) {
    const minutes = sections.reduce((s, sec) => s + sumExerciseMinutesInSection(sec), 0)
    cum = minutes
    runCumulativeEnds.push(cum)
    runs.push({
      kind: 'legacy',
      phaseOrderIndex: 0,
      phaseTitle: null,
      minutes,
      sections,
      streams: null,
      globalOrderSections: sections,
    })
    return { mode: 'legacy', runs, totalMin: minutes, runCumulativeEnds }
  }

  for (const r of runsMeta) {
    const slice = sections.slice(r.start, r.end)
    if (r.phaseKind === 'whole_group') {
      const minutes = slice.reduce((s, sec) => s + sumExerciseMinutesInSection(sec), 0)
      cum += minutes
      runCumulativeEnds.push(cum)
      const phaseTitle = slice[0]?.planLoc?.phaseTitle ?? null
      runs.push({
        kind: 'whole_group',
        phaseOrderIndex: r.phaseOrderIndex,
        phaseTitle,
        minutes,
        sections: slice,
        streams: null,
        globalOrderSections: slice,
      })
    } else {
      const po = r.phaseOrderIndex
      const streamOrders = streamsForParallelPhaseOrders(slice, po)
      const streams = streamOrders.map((so) => {
        const idxs = sectionIndicesForParallelStream(slice, po, so)
        const streamSecs = idxs
          .map((i) => slice[i])
          .sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
        const first = streamSecs[0]
        const streamTitle = first?.planLoc?.streamTitle ?? null
        const minutes = streamSecs.reduce((s, sec) => s + sumExerciseMinutesInSection(sec), 0)
        return {
          streamOrder: so,
          streamTitle,
          minutes,
          sections: streamSecs,
          printStreamId: `p${po}-s${so}`,
        }
      })
      const minutes = slice.reduce((s, sec) => s + sumExerciseMinutesInSection(sec), 0)
      cum += minutes
      runCumulativeEnds.push(cum)
      const phaseTitle = slice[0]?.planLoc?.phaseTitle ?? null
      runs.push({
        kind: 'parallel',
        phaseOrderIndex: po,
        phaseTitle,
        minutes,
        streams,
        sections: null,
        globalOrderSections: slice,
      })
    }
  }

  return { mode: 'phased', runs, totalMin: cum, runCumulativeEnds }
}

/** @param {object} unit Trainingseinheit inkl. `sections`, optional `phases` (GET) */
export function buildPlanRunViewModel(unit) {
  return buildPlanRunViewModelFromSections(sectionsWithPlanLocForDisplay(unit))
}

function coachContextLabelForSection(sec, sectionsList) {
  const pl = sec?.planLoc
  if (!pl?.phaseKind) return 'Ablauf'
  if (pl.phaseKind === 'whole_group') {
    const pt = pl.phaseTitle != null && String(pl.phaseTitle).trim() ? String(pl.phaseTitle).trim() : null
    return pt ? `Ganzgruppe · ${pt}` : 'Ganzgruppe'
  }
  const pt = pl.phaseTitle != null && String(pl.phaseTitle).trim() ? String(pl.phaseTitle).trim() : `Phase ${pl.phaseOrderIndex ?? 0}`
  const so = pl.parallelStreamOrderIndex ?? 0
  const st = pl.streamTitle != null && String(pl.streamTitle).trim() ? String(pl.streamTitle).trim() : `Gruppe ${so + 1}`
  return `Parallel · ${pt} · ${st}`
}

export const COACH_ENTRY_BRANCH_GATE = 'branch_gate'

/** Normalisierte Stream-Wahl pro paralleler Phase (Phase-Index → Stream-Order). */
export function normalizeCoachBranchPicks(raw) {
  const out = {}
  if (!raw || typeof raw !== 'object') return out
  for (const [k, v] of Object.entries(raw)) {
    const pk = parseInt(String(k), 10)
    const sv = typeof v === 'number' ? v : parseInt(String(v), 10)
    if (Number.isFinite(pk) && Number.isFinite(sv)) out[pk] = sv
  }
  return out
}

/**
 * URL-/Dropdown-Fokus `coachFocus` in Pick-Map mergen (fest gewählter Stream für eine Phase).
 * @param {object} branchPicks Roh-Picks (z. B. aus Session)
 * @param {{ phaseOrder: number, streamOrder: number }|null} coachFocusUrl z. B. ?po=&so=
 */
export function mergeCoachBranchPicksWithUrlFocus(branchPicks, coachFocusUrl) {
  const m = normalizeCoachBranchPicks(branchPicks)
  if (
    coachFocusUrl != null &&
    Number.isFinite(coachFocusUrl.phaseOrder) &&
    Number.isFinite(coachFocusUrl.streamOrder)
  ) {
    m[coachFocusUrl.phaseOrder] = coachFocusUrl.streamOrder
  }
  return m
}

/** Kurzstring für SessionStorage-Schlüssel (Sortierung stabil). */
export function coachBranchPicksStepStorageSuffix(mergedPicks) {
  const keys = Object.keys(mergedPicks)
    .map((k) => parseInt(String(k), 10))
    .filter((n) => Number.isFinite(n))
    .sort((a, b) => a - b)
  if (!keys.length) return 'full'
  return keys.map((k) => `p${k}-s${mergedPicks[k]}`).join('_')
}

export function coachBranchPicksStorageKey(unitId) {
  return `sj_coach_branches_${unitId}`
}

/**
 * Index zum Springen: Co-Trainer-Link atBranch+preferSo — Gate falls noch offen, sonst erste Kachel im Stream.
 */
export function findCoachTimelineJumpIndexForPhase(timeline, phaseOrder, preferStreamOrder = null) {
  const po = Number(phaseOrder)
  if (!Number.isFinite(po) || !Array.isArray(timeline)) return -1
  const hint = preferStreamOrder != null && Number.isFinite(Number(preferStreamOrder)) ? Number(preferStreamOrder) : null
  if (hint != null) {
    const ixStream = timeline.findIndex(
      (e) =>
        e.entryKind !== COACH_ENTRY_BRANCH_GATE &&
        e.runMeta?.kind === 'parallel' &&
        e.runMeta.phaseOrderIndex === po &&
        e.runMeta.streamOrder === hint
    )
    if (ixStream >= 0) return ixStream
  }
  const ixGate = timeline.findIndex(
    (e) => e.entryKind === COACH_ENTRY_BRANCH_GATE && e.branchMeta?.phaseOrderIndex === po
  )
  return ixGate
}

/** Gruppiert die flache Coach-Timeline für den Trainingsrahmen (Überschriften + Einträge). */
export function coachOutlineGroupsFromTimeline(timeline) {
  const groups = []
  for (let ix = 0; ix < timeline.length; ix++) {
    const ent = timeline[ix]
    let mergeKey = `ix-${ix}`
    let heading = 'Ablauf'
    let sub = ''
    if (ent.entryKind === COACH_ENTRY_BRANCH_GATE) {
      const po = ent.branchMeta?.phaseOrderIndex ?? 0
      mergeKey = `gate-${po}`
      heading = 'Split · Gruppe wählen'
      const pt = ent.branchMeta?.phaseTitle
      sub =
        pt != null && String(pt).trim()
          ? String(pt).trim()
          : `Parallele Phase ${po}`
    } else {
      const rm = ent.runMeta
      if (rm?.kind === 'whole_group') {
        mergeKey = `wg-${rm.phaseOrderIndex}`
        const pl = ent.sec?.planLoc
        const ptt = pl?.phaseTitle
        heading = 'Ganzgruppe'
        sub = ptt != null && String(ptt).trim() ? String(ptt).trim() : ''
      } else if (rm?.kind === 'parallel' && rm.streamOrder != null) {
        mergeKey = `par-${rm.phaseOrderIndex}-s${rm.streamOrder}`
        const pl = ent.sec?.planLoc
        const ptt = pl?.phaseTitle
        const stt = pl?.streamTitle
        const ptl = ptt != null && String(ptt).trim() ? String(ptt).trim() : `Phase ${rm.phaseOrderIndex}`
        const stl = stt != null && String(stt).trim() ? String(stt).trim() : `Gruppe ${rm.streamOrder + 1}`
        heading = `Parallel · ${ptl}`
        sub = stl
      } else if (rm?.kind === 'legacy') {
        mergeKey = 'legacy'
        heading = 'Ablauf'
        sub = ''
      } else {
        mergeKey = `misc-${ix}`
      }
    }
    const prev = groups[groups.length - 1]
    if (!prev || prev.mergeKey !== mergeKey) {
      groups.push({ mergeKey, heading, sub, entries: [{ ix, ent }] })
    } else {
      prev.entries.push({ ix, ent })
    }
  }
  return groups
}

/**
 * Flache Coach-Reihenfolge. Pro paralleler Phase ohne Eintrag in branchPicks: ein sichtbarer branch_gate,
 * damit keine verschränkten Split-Übungen, bis eine Gruppe gewählt wurde.
 * @param {object} unit
 * @param {object} branchPicks z. B. { 0: 1 } = Phase 0 → Stream 1
 */
export function flattenPlanTimeline(unit, branchPicks = {}) {
  const sections = sectionsWithPlanLocForDisplay(unit)
  const model = buildPlanRunViewModelFromSections(sections)
  if (model.mode === 'empty') return []

  const picks = normalizeCoachBranchPicks(branchPicks)
  const list = []

  const pushSectionItems = (sec, coachCtx, runMeta) => {
    const si = Math.max(0, sections.indexOf(sec))
    const secOrder = sec.order_index ?? si
    sortedItems(sec).forEach((item, ii) => {
      list.push({
        si,
        ii,
        secOrder,
        flatIndex: list.length,
        sec,
        item,
        coachContext: coachCtx,
        runMeta: runMeta || null,
      })
    })
  }

  for (const run of model.runs) {
    if (run.kind === 'legacy') {
      const meta = { kind: 'legacy', phaseOrderIndex: 0, streamOrder: null }
      for (const sec of run.globalOrderSections) {
        pushSectionItems(sec, coachContextLabelForSection(sec, sections), meta)
      }
      continue
    }
    if (run.kind === 'whole_group') {
      const meta = { kind: 'whole_group', phaseOrderIndex: run.phaseOrderIndex, streamOrder: null }
      for (const sec of run.globalOrderSections) {
        pushSectionItems(sec, coachContextLabelForSection(sec, sections), meta)
      }
      continue
    }
    if (run.kind === 'parallel') {
      const po = run.phaseOrderIndex
      const chosen = picks[po]
      const hasPick = chosen !== undefined && chosen !== null && Number.isFinite(Number(chosen))
      if (!hasPick) {
        list.push({
          entryKind: COACH_ENTRY_BRANCH_GATE,
          si: -1,
          ii: -1,
          secOrder: -1,
          flatIndex: list.length,
          sec: null,
          item: null,
          coachContext: '',
          branchMeta: {
            phaseOrderIndex: po,
            phaseTitle: run.phaseTitle,
            streams: run.streams || [],
          },
          runMeta: { kind: 'parallel', phaseOrderIndex: po, streamOrder: null },
        })
      } else {
        const st = run.streams?.find((x) => x.streamOrder === Number(chosen))
        const meta = { kind: 'parallel', phaseOrderIndex: po, streamOrder: Number(chosen) }
        if (st?.sections?.length) {
          for (const sec of st.sections) {
            pushSectionItems(sec, coachContextLabelForSection(sec, sections), meta)
          }
        }
      }
    }
  }

  return list
}

/** Optionen für Coach-Stream-Auswahl (phases · Gruppe). */
export function listCoachStreamFocusOptions(unit) {
  const model = buildPlanRunViewModelFromSections(sectionsWithPlanLocForDisplay(unit))
  const opts = []
  for (const run of model.runs) {
    if (run.kind !== 'parallel' || !run.streams?.length) continue
    const phaseLabel =
      run.phaseTitle != null && String(run.phaseTitle).trim()
        ? String(run.phaseTitle).trim()
        : `Phase ${run.phaseOrderIndex}`
    for (const st of run.streams) {
      const gl =
        st.streamTitle != null && String(st.streamTitle).trim()
          ? String(st.streamTitle).trim()
          : `Gruppe ${st.streamOrder + 1}`
      opts.push({
        phaseOrder: run.phaseOrderIndex,
        streamOrder: st.streamOrder,
        label: `${phaseLabel} · ${gl}`,
        valueKey: `${run.phaseOrderIndex}-${st.streamOrder}`,
      })
    }
  }
  return opts
}

/** Alle Ist-Minuten-Overrides aus lokalem Delta-State für PUT (unabhängig von gefilterter Coach-Timeline). */
export function durationOverridesMapFromDeltas(unit, deltas) {
  const out = {}
  if (!unit || !deltas || typeof deltas !== 'object') return out
  const sections = sortedSections(unit)
  sections.forEach((sec, si) => {
    const secOrder = sec.order_index ?? si
    sortedItems(sec).forEach((it, ii) => {
      if (it.item_type !== 'exercise' || it.id == null) return
      const k = itemStableKey(it, secOrder, ii)
      const dv = deltas[k]?.actual_duration_min
      if (dv !== undefined && dv !== '' && dv !== null && !Number.isNaN(Number(dv))) {
        out[String(it.id)] = { actual_duration_min: Number(dv) }
      }
    })
  })
  return out
}

/** PUT-Body für Coach-Speichern: `phases` wenn Plan Phasen hat, sonst `sections` (wie Planungseditor). */
export function buildCoachSavePlanPayload(unit, durationOverridesByItemId = {}) {
  const withLoc = sectionsWithPlanLocForDisplay(unit)
  const withDur = withLoc.map((sec) => ({
    ...sec,
    items: sortedItems(sec).map((it) => {
      if (it.item_type !== 'exercise' || it.id == null) return it
      const o = durationOverridesByItemId[String(it.id)]
      const av = o?.actual_duration_min
      if (av !== undefined && av !== '' && av !== null && Number.isFinite(Number(av))) {
        return { ...it, actual_duration_min: Number(av) }
      }
      return it
    }),
  }))
  return buildPlanPayloadForSave(withDur)
}

/**
 * Nach dem letzten Block eines Streams: Rückfrage, wenn die parallele Phase mehrere Gruppen hat.
 */
export function coachShouldPromptSplitRejoin(unit, lastTimelineEntry) {
  const rm = lastTimelineEntry?.runMeta
  if (!rm || rm.kind !== 'parallel' || rm.streamOrder == null) return null
  const model = buildPlanRunViewModelFromSections(sectionsWithPlanLocForDisplay(unit))
  const run = model.runs.find((r) => r.kind === 'parallel' && r.phaseOrderIndex === rm.phaseOrderIndex)
  if (!run?.streams || run.streams.length <= 1) return null
  return {
    phaseOrderIndex: run.phaseOrderIndex,
    phaseTitle: run.phaseTitle,
    streams: run.streams,
  }
}

/**
 * Nach dem letzten Block eines gewählten Streams: Rückfrage vor Ganzgruppenphase oder vor dem nächsten Split,
 * wenn die aktuelle Parallelphase mehrere Streams hat.
 */
export function coachShouldPromptSplitRejoinTransition(unit, currentEntry, nextEntry) {
  if (!currentEntry || !nextEntry) return null
  const cRm = currentEntry.runMeta
  if (!cRm || cRm.kind !== 'parallel' || cRm.streamOrder == null) return null
  const intoWholeGroup = nextEntry.runMeta?.kind === 'whole_group'
  const intoNextSplit = nextEntry.entryKind === COACH_ENTRY_BRANCH_GATE
  if (!intoWholeGroup && !intoNextSplit) return null
  return coachShouldPromptSplitRejoin(unit, currentEntry)
}

export function summarizeTimelineEntry(ent) {
  if (!ent) return ''
  if (ent.entryKind === COACH_ENTRY_BRANCH_GATE) {
    const t = ent.branchMeta?.phaseTitle != null && String(ent.branchMeta.phaseTitle).trim()
      ? String(ent.branchMeta.phaseTitle).trim()
      : ''
    return t ? `Split wählen · ${t}` : 'Split · Gruppe wählen'
  }
  const { item } = ent
  if (!item) return ''
  if (item.item_type === 'note') {
    const t = String(item.note_body || '').trim()
    return t.length > 72 ? `${t.slice(0, 70)}…` : t || 'Notiz'
  }
  const title = item.exercise_title || (item.exercise_id ? `Übung #${item.exercise_id}` : 'Übung')
  const vn = item.exercise_variant_name ? ` · ${item.exercise_variant_name}` : ''
  return `${title}${vn}`
}

/** Payload für PUT (schließt bestehendes Unit mit optionalen Overrides pro Abschnitt-Item-ID ab). */
export function sectionsToPutPayload(unit, durationOverridesByItemId = {}) {
  return sortedSections(unit).map((sec, si) => ({
    order_index: sec.order_index ?? si,
    title: ((sec.title || '').trim() || 'Abschnitt'),
    guidance_notes: sec.guidance_notes?.trim() ? sec.guidance_notes.trim() : null,
    ...(sec.source_template_section_id != null
      ? { source_template_section_id: sec.source_template_section_id }
      : {}),
    items: sortedItems(sec)
      .map((it, ii) => {
        if (it.item_type === 'note') {
          return {
            item_type: 'note',
            order_index: it.order_index ?? ii,
            note_body: it.note_body ?? '',
          }
        }
        const eid = it.exercise_id
        if (eid === '' || eid == null || Number.isNaN(Number(eid))) {
          return null
        }
        const isCombo =
          String(it.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
        const vid = isCombo ? null : it.exercise_variant_id
        let actual =
          durationOverridesByItemId[String(it.id)]?.actual_duration_min ??
          it.actual_duration_min
        if (actual === '' || actual === undefined) actual = null
        else actual = typeof actual === 'number' ? actual : parseInt(String(actual), 10)
        if (actual !== null && !Number.isFinite(actual)) actual = null

        const row = {
          item_type: 'exercise',
          order_index: it.order_index ?? ii,
          exercise_id: parseInt(String(eid), 10),
          exercise_variant_id:
            vid !== '' && vid != null && !Number.isNaN(Number(vid)) ? parseInt(String(vid), 10) : null,
          planned_duration_min: coalescePositiveInt(it.planned_duration_min),
          actual_duration_min: actual,
          notes: trimOrNull(it.notes),
          modifications: trimOrNull(it.modifications),
        }
        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) {
              row.planning_method_profile = cleaned
            }
          }
        }
        return row
      })
      .filter(Boolean),
  }))
}

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

function trimOrNull(v) {
  const s = v != null ? String(v).trim() : ''
  return s ? s : null
}
