/* SportK — Motor de timer multi-fase con resolución sub-segundo
   Hook useTimerEngine(routine, options) basado en Web Worker + rAF.
   Cada ejercicio se descompone en una secuencia de segmentos
   { phase, ms, intensity, cueKey, cueParams?, untilTap? }.
   El campo cueKey se resuelve a string vía window.t para soporte i18n.
   Si untilTap=true, el segmento NO avanza por tiempo: espera a que el
   consumidor llame controls.tapPONR() (usado por la fase 'arousal' del
   ciclo Stop-Start clínico en edging-hold).
   Soporta tipos dinámicos (flick, ladder, elevator, wave, knack,
   edging-hold, edge, reverse). Emite onPhaseChange para feedback.

   Reloj: el avance de fase se ancla a performance.now() (wall-clock),
   y los ticks llegan desde un Web Worker con setInterval(100ms). Los
   Web Workers no se *throttlean* por visibilidad como rAF, así que el
   timer sigue corriendo aunque la pestaña esté minimizada o en segundo
   plano. rAF se mantiene en paralelo solo para suavizar la animación
   visual cuando la pestaña está visible.
*/

const { useState: useStateE, useEffect: useEffectE, useRef: useRefE, useMemo: useMemoE, useCallback: useCallbackE } = React;

// ---------- Generadores de segmentos por tipo ----------

// Cada segmento usa `cueKey` (clave i18n) + `cueParams` (vars opcionales) en lugar de
// texto hardcoded. El hook resuelve la clave a string vía `window.t(cueKey, cueParams)`
// para mantener el motor agnóstico al idioma activo.

// Descanso unificado entre reps de cualquier ejercicio: 3s fijos.
// Las pausas más largas (15s post-edge) saturaban el ritmo; el usuario quiere flujo.
const REST_MS = 3000;

const READY_SEGMENTS = [
  { phase: "ready", ms: REST_MS, intensity: 0, cueKey: "phase.ready" },
];

const REVERSE_KEGEL_SEGMENTS = [
  { phase: "reverse", ms: 3000, intensity: -50, cueKey: "engine.push_gently_down" },
  { phase: "rest", ms: REST_MS, intensity: 0, cueKey: "engine.relax_full" },
];

function buildStandardSegments(ex) {
  if (ex.segments && ex.segments.length) return ex.segments;
  const hold = ex.holdSec || 5;
  return [
    { phase: "contract", ms: 500, intensity: 100, cueKey: "phase.contract" },
    { phase: "hold", ms: hold * 1000, intensity: 100, cueKey: "phase.hold" },
    { phase: "release", ms: 1000, intensity: 0, cueKey: "engine.release_slow" },
    { phase: "rest", ms: REST_MS, intensity: 0, cueKey: "phase.rest" },
  ];
}

function buildFlickSegments(ex) {
  const onMs = (ex.params && ex.params.onMs) || 400;
  const offMs = (ex.params && ex.params.offMs) || 600;
  return [
    { phase: "contract", ms: onMs, intensity: 100, cueKey: "phase.contract" },
    { phase: "rest", ms: offMs, intensity: 0, cueKey: "phase.release" },
  ];
}

function buildLadderSegments(ex, repIdx) {
  const startSec = (ex.params && ex.params.startSec) || 2;
  const maxSec = (ex.params && ex.params.maxSec) || 8;
  const hold = Math.min(startSec + repIdx, maxSec);
  const intensity = Math.min(50 + repIdx * 10, 100);
  return [
    { phase: "hold", ms: hold * 1000, intensity, cueKey: "engine.ladder_hold", cueParams: { sec: hold, n: repIdx + 1 } },
    { phase: "release", ms: 1200, intensity: 0, cueKey: "engine.descend_slow" },
  ];
}

function buildElevatorSegments(ex) {
  const stepHold = ((ex.params && ex.params.stepHoldMs) || 2000);
  return [
    { phase: "hold", ms: stepHold, intensity: 25, cueKey: "engine.elevator_floor", cueParams: { n: 1, pct: 25 } },
    { phase: "hold", ms: stepHold, intensity: 50, cueKey: "engine.elevator_floor", cueParams: { n: 2, pct: 50 } },
    { phase: "hold", ms: stepHold, intensity: 75, cueKey: "engine.elevator_floor", cueParams: { n: 3, pct: 75 } },
    { phase: "peak", ms: stepHold, intensity: 100, cueKey: "engine.elevator_floor", cueParams: { n: 4, pct: 100 } },
    { phase: "hold", ms: stepHold, intensity: 75, cueKey: "engine.elevator_floor", cueParams: { n: 3, pct: 75 } },
    { phase: "hold", ms: stepHold, intensity: 50, cueKey: "engine.elevator_floor", cueParams: { n: 2, pct: 50 } },
    { phase: "hold", ms: stepHold, intensity: 25, cueKey: "engine.elevator_floor", cueParams: { n: 1, pct: 25 } },
    { phase: "release", ms: 1000, intensity: 0, cueKey: "engine.elevator_release_full" },
  ];
}

function buildWaveSegments(ex) {
  // Onda Pompoir: alternar anterior (entrada vaginal / base pene) y posterior (profundo).
  const segMs = (ex.params && ex.params.segMs) || 800;
  const restMs = REST_MS;
  return [
    { phase: "contract", ms: segMs, intensity: 80, cueKey: "engine.wave_front" },
    { phase: "contract", ms: segMs, intensity: 80, cueKey: "engine.wave_back" },
    { phase: "contract", ms: segMs, intensity: 90, cueKey: "engine.wave_front_short" },
    { phase: "contract", ms: segMs, intensity: 90, cueKey: "engine.wave_back_short" },
    { phase: "release", ms: 1000, intensity: 0, cueKey: "phase.release" },
    { phase: "rest", ms: restMs, intensity: 0, cueKey: "phase.rest" },
  ];
}

function buildKnackSegments(ex) {
  // The Knack: contracción anticipatoria + tos simulada + sostén post-tos.
  // Flujo: Prepárate → ¡Aprieta! → Tose Xs sin soltar → Mantenlo Xs sin toser → suelta → descanso.
  const holdMs = (ex.params && ex.params.holdMs) || 1500;
  const restMs = REST_MS;
  const sec = Math.max(1, Math.round(holdMs / 1000));
  return [
    { phase: "ready", ms: 1500, intensity: 0, cueKey: "phase.ready" },
    { phase: "contract", ms: 700, intensity: 100, cueKey: "engine.knack_squeeze" },
    { phase: "knack-trigger", ms: holdMs, intensity: 100, cueKey: "engine.knack_cough", cueParams: { sec } },
    { phase: "knack-hold", ms: holdMs, intensity: 100, cueKey: "engine.knack_hold", cueParams: { sec } },
    { phase: "release", ms: 800, intensity: 0, cueKey: "phase.release" },
    { phase: "rest", ms: restMs, intensity: 0, cueKey: "engine.recover" },
  ];
}

function buildEdgingHoldSegments(ex) {
  // Hold sostenido al N% MVC con respiración 4-7-8 superpuesta. Para control eyaculatorio / edging.
  // params.intensity (default 50) ajusta el % MVC del hold; params.flickBurstSec (default 0)
  // prepende una ráfaga de flicks (onMs/offMs configurables) para simular el ramp-up de excitación
  // ANTES del hold, transformando el ejercicio en un ciclo Stop-Start completo (flicks → hold).
  // params.flickAfterSec (default 0) hace lo simétrico: añade una ráfaga DESPUÉS del hold
  // (patrón "edge → flicker"): el squeeze frena el reflejo y los flicks mantienen la excitación
  // sin permitir que decaiga, entrenando control sostenido bajo arousal continuo.
  // Si ex.withArousal === true, se prepende una fase 'arousal' controlada por tap del usuario:
  // simula el ciclo Stop-Start clínico (Masters & Johnson / MTCK): el usuario sube hasta el
  // PONR por su cuenta y pulsa para iniciar el Kegel.
  const holdSec = (ex.params && ex.params.holdSec) || 30;
  const intensity = (ex.params && ex.params.intensity) || 50;
  const flickBurstSec = (ex.params && ex.params.flickBurstSec) || 0;
  const flickAfterSec = (ex.params && ex.params.flickAfterSec) || 0;
  const flickOnMs = (ex.params && ex.params.flickOnMs) || 400;
  const flickOffMs = (ex.params && ex.params.flickOffMs) || 600;
  const segs = [];
  if (ex.withArousal) {
    segs.push({
      phase: "arousal",
      ms: Infinity,
      untilTap: true,
      intensity: 0,
      cueKey: "engine.arousal_cue",
    });
  }
  if (flickBurstSec > 0) {
    const cycle = flickOnMs + flickOffMs;
    const nFlicks = Math.max(1, Math.round((flickBurstSec * 1000) / cycle));
    segs.push({ phase: "ready", ms: 600, intensity: 0, cueKey: "engine.flick_burst", cueParams: { sec: flickBurstSec } });
    for (let i = 0; i < nFlicks; i++) {
      segs.push({ phase: "contract", ms: flickOnMs, intensity: 100, cueKey: "phase.contract" });
      segs.push({ phase: "rest", ms: flickOffMs, intensity: 0, cueKey: "phase.release" });
    }
  }
  segs.push(
    { phase: "contract", ms: 700, intensity, cueKey: "engine.squeeze_pct", cueParams: { pct: intensity } },
    { phase: "hold", ms: holdSec * 1000, intensity, cueKey: "engine.edge_hold", cueParams: { sec: holdSec, pct: intensity } },
    { phase: "release", ms: 1500, intensity: 0, cueKey: "engine.release_slow" },
  );
  if (flickAfterSec > 0) {
    const cycle = flickOnMs + flickOffMs;
    const nFlicks = Math.max(1, Math.round((flickAfterSec * 1000) / cycle));
    segs.push({ phase: "ready", ms: 600, intensity: 0, cueKey: "engine.flick_after", cueParams: { sec: flickAfterSec } });
    for (let i = 0; i < nFlicks; i++) {
      segs.push({ phase: "contract", ms: flickOnMs, intensity: 100, cueKey: "phase.contract" });
      segs.push({ phase: "rest", ms: flickOffMs, intensity: 0, cueKey: "phase.release" });
    }
  }
  segs.push(
    { phase: "rest", ms: REST_MS, intensity: 0, cueKey: "phase.rest" },
  );
  return segs;
}

function buildEdgeSegments(ex) {
  // Edge SIN hold sostenido — squeeze technique clásica de Masters & Johnson (1970):
  // arousal hasta PONR → squeeze breve y firme (freno reflejo) → release inmediato → flicks opcionales.
  // No es endurance muscular (eso lo hace edging-hold) sino entrenamiento del REFLEJO de freno
  // y de la respuesta parasimpática. Squeeze de 1.5-3s @100% MVC sin sostener.
  // params: squeezeMs (default 2000), flickAfterSec (default 0 → si >0 añade flicker post-squeeze
  // para mantener el arousal bajo control sin permitir que decaiga).
  const squeezeMs = (ex.params && ex.params.squeezeMs) || 2000;
  const intensity = (ex.params && ex.params.intensity) || 100;
  const flickAfterSec = (ex.params && ex.params.flickAfterSec) || 0;
  const flickOnMs = (ex.params && ex.params.flickOnMs) || 400;
  const flickOffMs = (ex.params && ex.params.flickOffMs) || 600;
  const segs = [];
  if (ex.withArousal) {
    segs.push({
      phase: "arousal",
      ms: Infinity,
      untilTap: true,
      intensity: 0,
      cueKey: "engine.arousal_cue",
    });
  }
  segs.push(
    { phase: "contract", ms: squeezeMs, intensity, cueKey: "engine.edge_squeeze", cueParams: { pct: intensity } },
    { phase: "release", ms: 1000, intensity: 0, cueKey: "engine.release_slow" },
  );
  if (flickAfterSec > 0) {
    const cycle = flickOnMs + flickOffMs;
    const nFlicks = Math.max(1, Math.round((flickAfterSec * 1000) / cycle));
    segs.push({ phase: "ready", ms: 600, intensity: 0, cueKey: "engine.flick_after", cueParams: { sec: flickAfterSec } });
    for (let i = 0; i < nFlicks; i++) {
      segs.push({ phase: "contract", ms: flickOnMs, intensity: 100, cueKey: "phase.contract" });
      segs.push({ phase: "rest", ms: flickOffMs, intensity: 0, cueKey: "phase.release" });
    }
  }
  segs.push(
    { phase: "rest", ms: REST_MS, intensity: 0, cueKey: "phase.rest" },
  );
  return segs;
}

function buildReverseSegments() {
  return [...REVERSE_KEGEL_SEGMENTS];
}

// ---------- Despacho ----------

function buildSegmentsForRep(ex, repIdx) {
  switch (ex.type) {
    case "flick": return buildFlickSegments(ex);
    case "ladder": return buildLadderSegments(ex, repIdx);
    case "elevator": return buildElevatorSegments(ex);
    case "wave": return buildWaveSegments(ex);
    case "knack": return buildKnackSegments(ex);
    case "edging-hold": return buildEdgingHoldSegments(ex);
    case "edge": return buildEdgeSegments(ex);
    case "reverse": return buildReverseSegments();
    case "standard":
    default:
      return buildStandardSegments(ex);
  }
}

// Decide si esta rep es "reverse intercalada" (anti-hipertonía).
function isReverseRep(ex, repIdx) {
  if (!ex.reverseEvery || ex.reverseEvery <= 0) return false;
  if (ex.type === "reverse") return false; // ya es reverse
  // 1-indexed: intercalar después de cada N reps normales.
  return (repIdx + 1) % (ex.reverseEvery + 1) === 0;
}

// Resuelve el cue de un segmento al idioma actual usando window.t (con fallback).
function resolveSegmentCue(seg) {
  if (!seg || !seg.cueKey) return "";
  const t = window.t;
  if (typeof t !== "function") return "";
  return t(seg.cueKey, seg.cueParams || undefined) || "";
}

// ---------- Hook ----------

function useTimerEngine(routine, opts = {}) {
  const onPhaseChange = opts.onPhaseChange || null;
  const onComplete = opts.onComplete || null;
  const onTick = opts.onTick || null;

  const exercises = (routine && routine.exercises) || [];

  const [exerciseIdx, setExerciseIdx] = useStateE(0);
  const [repIdx, setRepIdx] = useStateE(0);
  const [segmentIdx, setSegmentIdx] = useStateE(0);
  const [msIntoPhase, setMsIntoPhase] = useStateE(0);
  const [paused, setPaused] = useStateE(false);
  const [done, setDone] = useStateE(false);
  const [startedAt] = useStateE(() => performance.now());
  const [reverseInjected, setReverseInjected] = useStateE(false);
  // estadísticas de la sesión
  const sessionStatsRef = useRefE({
    perExercise: exercises.map(() => ({ completedReps: 0, completedSegments: 0 })),
    totalCompletedReps: 0,
    elapsedMs: 0,
  });

  const ex = exercises[exerciseIdx] || null;

  // Ejercicio efectivo en esta rep (puede ser un reverse intercalado).
  const effectiveType = useMemoE(() => {
    if (!ex) return null;
    if (reverseInjected) return "reverse";
    if (isReverseRep(ex, repIdx)) return "reverse";
    return ex.type || "standard";
  }, [ex, repIdx, reverseInjected]);

  // Segmentos de la rep actual.
  // Política de respiración: los generadores por-rep NO emiten cues de inhale/exhale —
  // el trabajo (contract/hold/release) corre limpio y el usuario respira a su ritmo. El
  // único punto donde se emite un ciclo completo de respiración es el bridge de la última
  // rep de cada ejercicio (inhale + exhale tras el rest final), que sirve como transición
  // entre ejercicios.
  const segments = useMemoE(() => {
    if (!ex) return [];
    const totalReps = ex.reps || 1;
    let segs = effectiveType === "reverse" ? buildReverseSegments() : buildSegmentsForRep(ex, repIdx);
    const isInjectedReverse = effectiveType === "reverse" && reverseInjected;
    const isLast = !isInjectedReverse && repIdx === totalReps - 1;
    if (isLast) {
      // Bridge respiratorio entre ejercicios: ciclo completo (inhale + exhale) tras el rest final.
      // Es el ÚNICO punto del flujo donde se emiten cues de respiración — los generadores
      // por-rep no incluyen inhale/exhale para mantener el ritmo del trabajo limpio.
      segs = [
        ...segs,
        { phase: "inhale", ms: 1500, intensity: 0, cueKey: "phase.inhale" },
        { phase: "exhale", ms: 1500, intensity: 0, cueKey: "phase.exhale" },
      ];
    }
    return segs;
  }, [ex, repIdx, effectiveType, reverseInjected]);

  const segment = segments[segmentIdx] || null;

  // Reloj wall-clock (anclado al inicio de cada fase) + worker para ticks en background
  const phaseStartTsRef = useRefE(0);
  const tickWorkerRef = useRefE(null);
  const rafRef = useRefE(0);
  const phaseChangeRef = useRefE(null);
  const repsCompletedThisExerciseRef = useRefE(0);

  // Epoch que aumenta en cada cambio de segmento. Sirve como guard contra ticks "stale":
  // entre que un tick llama advance() y que la cleanup del effect remueva el listener
  // del worker, puede entrar otro tick con el `segment` viejo capturado en su closure
  // que dispararía un advance() extra y saltaría la fase recién entrada (típicamente la
  // fase 'arousal' al iniciar un edge / edging-hold). Cada tick captura el epoch al
  // setup del effect y aborta si el ref ya avanzó.
  const phaseEpochRef = useRefE(0);

  // Web Worker que emite ticks cada 100ms. A diferencia de rAF, no se *throttlea*
  // cuando la pestaña pierde foco — garantiza que el timer siga corriendo en background.
  useEffectE(() => {
    let worker = null;
    let blobUrl = null;
    try {
      const workerSource = "let id=null;self.onmessage=function(e){if(e.data&&e.data.type==='start'){if(id)clearInterval(id);id=setInterval(function(){self.postMessage({type:'tick'});},e.data.ms||100);}else if(e.data&&e.data.type==='stop'){if(id){clearInterval(id);id=null;}}};";
      const blob = new Blob([workerSource], { type: "application/javascript" });
      blobUrl = URL.createObjectURL(blob);
      worker = new Worker(blobUrl);
      tickWorkerRef.current = worker;
    } catch (e) {
      tickWorkerRef.current = null;
    }
    return () => {
      try { if (worker) { worker.postMessage({ type: "stop" }); worker.terminate(); } } catch (e) {}
      try { if (blobUrl) URL.revokeObjectURL(blobUrl); } catch (e) {}
      tickWorkerRef.current = null;
    };
  }, []);

  // Cada cambio de segmento fija el ancla wall-clock para esta fase y avanza el epoch.
  useEffectE(() => {
    phaseStartTsRef.current = performance.now();
    phaseEpochRef.current += 1;
  }, [segmentIdx, repIdx, exerciseIdx, reverseInjected]);

  // Pausa/Reanudación: cuando el usuario reanuda, re-anclamos el ancla wall-clock para
  // que el tiempo en pausa no cuente como progreso de fase. Restamos el msIntoPhase
  // ya acumulado para preservar el avance previo.
  const pausedAtMsRef = useRefE(0);
  useEffectE(() => {
    if (paused) {
      pausedAtMsRef.current = msIntoPhase;
    } else {
      // Al reanudar, anclar al pasado para conservar msIntoPhase ya acumulado.
      phaseStartTsRef.current = performance.now() - pausedAtMsRef.current;
    }
    // eslint-disable-next-line
  }, [paused]);

  // Disparar evento de cambio de fase
  useEffectE(() => {
    if (!segment || done) return;
    const phase = segment.phase;
    if (phaseChangeRef.current !== phase) {
      phaseChangeRef.current = phase;
      if (onPhaseChange) {
        onPhaseChange(phase, {
          intensity: segment.intensity,
          cue: resolveSegmentCue(segment),
          cueKey: segment.cueKey,
          ms: segment.ms,
          repIdx,
          exerciseIdx,
          isReverse: effectiveType === "reverse",
          untilTap: !!segment.untilTap,
        });
      }
    }
  }, [segmentIdx, exerciseIdx, repIdx, effectiveType, done]);

  // Loop principal: anclado a wall-clock (performance.now() − phaseStartTsRef).
  // Dos fuentes de ticks corren en paralelo:
  //   - Worker setInterval(100ms): garantiza progreso aunque la pestaña esté en background.
  //   - rAF: solo para suavizar la animación cuando la pestaña está visible.
  // Ambas llaman a la misma función de tick, que es idempotente respecto al wall-clock.
  useEffectE(() => {
    if (paused || done || !segment) return;
    // lastReportedTs vive en tiempo real (no en wall-clock anclado al inicio de fase)
    // para que el stat global elapsedMs cuente solo segundos activos, no el tiempo
    // simulado que añade pause/resume al re-anclar phaseStartTsRef al pasado.
    let lastReportedTs = performance.now();
    // Capturamos el epoch al setup del effect. Si el ref avanza (porque ya entramos a
    // otra fase) y un tick "en vuelo" del worker o rAF anterior llega aquí, lo descartamos
    // antes de tocar advance() — evita el salto involuntario de fases (p.ej. saltarse
    // arousal al entrar a un edge).
    const myEpoch = phaseEpochRef.current;

    const tick = () => {
      if (phaseEpochRef.current !== myEpoch) return;
      const now = performance.now();
      const dt = now - lastReportedTs;
      lastReportedTs = now;
      sessionStatsRef.current.elapsedMs += Math.max(0, dt);

      const elapsed = now - phaseStartTsRef.current;
      // Segmentos controlados por tap (p.ej. fase 'arousal'): no auto-avanzan.
      if (segment.untilTap) {
        setMsIntoPhase(elapsed);
        if (onTick) onTick(elapsed, 0, segment);
        return;
      }
      if (elapsed >= segment.ms) {
        advance();
        return;
      }
      setMsIntoPhase(elapsed);
      if (onTick) onTick(elapsed, segment.ms, segment);
    };

    // Tick worker (background-safe).
    let onWorkerMessage = null;
    const w = tickWorkerRef.current;
    if (w) {
      onWorkerMessage = (e) => { if (e && e.data && e.data.type === "tick") tick(); };
      w.addEventListener("message", onWorkerMessage);
      try { w.postMessage({ type: "start", ms: 100 }); } catch (e) {}
    }

    // rAF (solo para suavidad visual cuando la pestaña está visible).
    const rafTick = () => {
      tick();
      rafRef.current = requestAnimationFrame(rafTick);
    };
    rafRef.current = requestAnimationFrame(rafTick);

    return () => {
      cancelAnimationFrame(rafRef.current);
      if (w && onWorkerMessage) {
        try { w.removeEventListener("message", onWorkerMessage); } catch (e) {}
        try { w.postMessage({ type: "stop" }); } catch (e) {}
      }
    };
    // eslint-disable-next-line
  }, [paused, done, segmentIdx, repIdx, exerciseIdx, segment && segment.ms, segment && segment.untilTap]);

  // Avance de segmento → rep → ejercicio
  const advance = useCallbackE(() => {
    // Invalida cualquier tick "en vuelo" del effect anterior (rAF o worker) ANTES de
    // tocar setState. Los ticks viejos comparan su `myEpoch` capturado con este ref;
    // tras el bump verán mismatch y abortarán antes de llamar advance() otra vez.
    phaseEpochRef.current += 1;
    setSegmentIdx((sIdx) => {
      const segsNow = segments;
      const lastSeg = sIdx + 1 >= segsNow.length;
      if (!lastSeg) return sIdx + 1;

      // Cerró todos los segmentos de la rep actual
      // Si fue una rep reverse intercalada, vuelve a la rep normal.
      if (effectiveType === "reverse" && reverseInjected) {
        setReverseInjected(false);
        return 0;
      }

      // Acumular estadísticas
      sessionStatsRef.current.totalCompletedReps += 1;
      const peStats = sessionStatsRef.current.perExercise[exerciseIdx];
      if (peStats) peStats.completedReps += 1;
      repsCompletedThisExerciseRef.current += 1;

      const totalReps = (ex && ex.reps) || 1;
      // ¿Última rep del ejercicio?
      if (repIdx + 1 >= totalReps) {
        // ¿Necesita reverse final intercalado? (si reverseEvery > 0, hacer una más al final)
        if (ex && ex.reverseEvery > 0 && !reverseInjected && (totalReps % ex.reverseEvery !== 0)) {
          setReverseInjected(true);
          return 0;
        }
        // Pasar al siguiente ejercicio
        const nextExIdx = exerciseIdx + 1;
        if (nextExIdx >= exercises.length) {
          setDone(true);
          if (onComplete) {
            onComplete({
              elapsedMs: sessionStatsRef.current.elapsedMs,
              totalCompletedReps: sessionStatsRef.current.totalCompletedReps,
              perExercise: sessionStatsRef.current.perExercise,
              completedExercises: exercises.length,
            });
          }
          return 0;
        }
        setExerciseIdx(nextExIdx);
        setRepIdx(0);
        repsCompletedThisExerciseRef.current = 0;
        return 0;
      }

      // ¿Toca rep reverse intercalada antes de la siguiente normal?
      // Avanzamos el repIdx ANTES de inyectar el reverse, así al terminar
      // el reverse no repetimos la rep que acabamos de cerrar.
      if (ex && ex.reverseEvery > 0 && !reverseInjected) {
        const completed = repIdx + 1; // reps normales completadas
        if (completed % ex.reverseEvery === 0) {
          setReverseInjected(true);
          setRepIdx((r) => r + 1);
          return 0;
        }
      }

      setRepIdx((r) => r + 1);
      return 0;
    });
  }, [segments, effectiveType, reverseInjected, repIdx, exerciseIdx, ex, exercises.length, onComplete]);

  // Controles
  const pause = useCallbackE(() => setPaused(true), []);
  const resume = useCallbackE(() => setPaused(false), []);
  const toggle = useCallbackE(() => setPaused((p) => !p), []);

  // Tap de PONR (Punto de no retorno) — avanza una fase 'arousal' / cualquier untilTap.
  const tapPONR = useCallbackE(() => {
    if (!segment || !segment.untilTap) return;
    phaseStartTsRef.current = performance.now();
    setMsIntoPhase(0);
    advance();
  }, [segment, advance]);

  const skipSegment = useCallbackE(() => {
    phaseStartTsRef.current = performance.now();
    setMsIntoPhase(0);
    advance();
  }, [advance]);

  const skipExercise = useCallbackE(() => {
    if (!ex) return;
    const nextExIdx = exerciseIdx + 1;
    if (nextExIdx >= exercises.length) {
      setDone(true);
      if (onComplete) {
        onComplete({
          elapsedMs: sessionStatsRef.current.elapsedMs,
          totalCompletedReps: sessionStatsRef.current.totalCompletedReps,
          perExercise: sessionStatsRef.current.perExercise,
          completedExercises: exerciseIdx + 1,
        });
      }
      return;
    }
    phaseEpochRef.current += 1;
    phaseStartTsRef.current = performance.now();
    setExerciseIdx(nextExIdx);
    setRepIdx(0);
    setSegmentIdx(0);
    setMsIntoPhase(0);
    setReverseInjected(false);
  }, [ex, exerciseIdx, exercises.length, onComplete]);

  const back = useCallbackE(() => {
    // Vuelve una rep o ejercicio.
    phaseEpochRef.current += 1;
    phaseStartTsRef.current = performance.now();
    if (reverseInjected) {
      setReverseInjected(false);
      setSegmentIdx(0);
      setMsIntoPhase(0);
      return;
    }
    if (repIdx > 0) {
      setRepIdx(repIdx - 1);
      setSegmentIdx(0);
      setMsIntoPhase(0);
      return;
    }
    if (exerciseIdx > 0) {
      setExerciseIdx(exerciseIdx - 1);
      const prev = exercises[exerciseIdx - 1];
      setRepIdx(Math.max(0, (prev.reps || 1) - 1));
      setSegmentIdx(0);
      setMsIntoPhase(0);
    }
  }, [reverseInjected, repIdx, exerciseIdx, exercises]);

  // Cancelar / abortar
  const abort = useCallbackE(() => {
    cancelAnimationFrame(rafRef.current);
    try {
      const w = tickWorkerRef.current;
      if (w) w.postMessage({ type: "stop" });
    } catch (e) {}
    setDone(true);
  }, []);

  // Progreso global de la rutina (0..1)
  const totalRepsPlanned = useMemoE(
    () => exercises.reduce((s, e) => s + (e.reps || 1), 0) || 1,
    [exercises]
  );
  const repsDone =
    exercises.slice(0, exerciseIdx).reduce((s, e) => s + (e.reps || 1), 0) + repIdx;
  const overallProgress = Math.min(1, repsDone / totalRepsPlanned);

  return {
    routine,
    exercise: ex,
    exerciseIdx,
    repIdx,
    segmentIdx,
    segment,
    segments,
    phase: segment ? segment.phase : "ready",
    cue: resolveSegmentCue(segment),
    cueKey: segment ? segment.cueKey : null,
    cueParams: segment ? segment.cueParams : null,
    intensity: segment ? segment.intensity : 0,
    msIntoPhase,
    msTotalPhase: segment ? segment.ms : 0,
    isReverseRep: effectiveType === "reverse",
    paused,
    done,
    overallProgress,
    repsDone,
    totalRepsPlanned,
    elapsedMs: sessionStatsRef.current.elapsedMs,
    sessionStats: sessionStatsRef.current,
    controls: { pause, resume, toggle, tapPONR, skipSegment, skipExercise, back, abort },
  };
}

window.useTimerEngine = useTimerEngine;
window.buildSegmentsForRep = buildSegmentsForRep;
window.REVERSE_KEGEL_SEGMENTS = REVERSE_KEGEL_SEGMENTS;
