import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';

import '../application/application.dart';
import '../domain/domain.dart';
import 'exercise_step_audio.dart';
import 'history_screen.dart';
import 'theme.dart';

enum WorkoutExecutionMode { active, rest, paused, finished }

typedef VideoMediaBuilder =
    Widget Function(BuildContext context, MediaAsset asset);

final class WorkoutExecutionScreen extends StatefulWidget {
  const WorkoutExecutionScreen({
    required this.initialSession,
    required this.activeUseCases,
    required this.closeUseCase,
    required this.historyUseCases,
    required this.workoutTemplateUseCases,
    this.mediaUseCases,
    this.mediaAssetLoader,
    this.videoMediaBuilder,
    this.stepUseCases,
    this.stepAudioCuePlayer,
    super.key,
  });

  final ActiveWorkoutSession initialSession;
  final ActiveWorkoutSessionUseCases activeUseCases;
  final ActiveExerciseStepUseCases? stepUseCases;
  final CloseWorkoutSessionUseCase closeUseCase;
  final WorkoutHistoryUseCases historyUseCases;
  final WorkoutTemplateUseCases workoutTemplateUseCases;
  final MediaUseCases? mediaUseCases;
  final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
  final VideoMediaBuilder? videoMediaBuilder;
  final ExerciseStepAudioCuePlayer? stepAudioCuePlayer;

  @override
  State<WorkoutExecutionScreen> createState() => _WorkoutExecutionScreenState();
}

final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
  late ActiveWorkoutSession _session;
  late final WorkoutExecutionPlan _plan;
  late WorkoutExecutionMode _mode;
  Timer? _ticker;
  Timer? _restTicker;
  Timer? _scoreStopwatchTicker;
  Timer? _stepTicker;
  Timer? _stepScoreTicker;
  WorkoutHistory? _completedHistory;
  String? _activeRestStateId;
  ActiveScoreStopwatchState? _scoreStopwatch;
  ActiveExerciseStepProgressView? _stepProgress;
  late final ExerciseStepAudioCuePlayer _stepAudioCuePlayer;
  late final bool _ownsStepAudioCuePlayer;
  final _stepScoreController = TextEditingController();
  int? _manualScoreTimeMs;
  DateTime? _stepScoreStartedAt;
  var _stepScoreAccumulatedMs = 0;
  var _stepScoreRunning = false;
  var _lastCountdownBeepSecond = -1;
  var _advancingStep = false;
  late DateTime _seriesStartedAt;
  var _reps = 0;
  final _scoreController = TextEditingController();
  var _remainingRestSeconds = 0;

  ExecutionPosition get _position => ExecutionPosition(
    programIndex: _session.currentProgramIndex,
    exerciseIndex: _session.currentExerciseIndex,
    setIndex: _session.currentSetIndex,
  );

  ExecutionExercise get _exercise => _plan.exerciseAt(_position);

  @override
  void initState() {
    super.initState();
    _session = widget.initialSession;
    _plan = WorkoutExecutionPlan.fromSession(_session);
    _ownsStepAudioCuePlayer =
        widget.stepAudioCuePlayer == null && widget.stepUseCases != null;
    _stepAudioCuePlayer =
        widget.stepAudioCuePlayer ??
        (widget.stepUseCases == null
            ? const NoOpExerciseStepAudioCuePlayer()
            : AudioplayersExerciseStepAudioCuePlayer());
    _mode = _session.status == ActiveWorkoutStatus.running
        ? WorkoutExecutionMode.active
        : WorkoutExecutionMode.paused;
    _reps = _initialRepsFor(_exercise);
    _seriesStartedAt = DateTime.now().toUtc();
    _ticker = Timer.periodic(const Duration(seconds: 1), (_) {
      if (mounted) setState(() {});
    });
    unawaited(_loadScoreStopwatch());
    unawaited(_loadStepProgress());
    unawaited(_restoreActiveRest());
  }

  @override
  void dispose() {
    _ticker?.cancel();
    _restTicker?.cancel();
    _scoreStopwatchTicker?.cancel();
    _stepTicker?.cancel();
    _stepScoreTicker?.cancel();
    if (_ownsStepAudioCuePlayer) {
      unawaited(_stepAudioCuePlayer.dispose());
    }
    _scoreController.dispose();
    _stepScoreController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      onPopInvoked: _handleSystemBack,
      child: Scaffold(
        appBar: AppBar(
          automaticallyImplyLeading: false,
          leading: _canPauseFromNavigation
              ? IconButton(
                  tooltip: 'Mettre en pause',
                  onPressed: () => unawaited(_pause()),
                  icon: const Icon(Icons.arrow_back),
                )
              : null,
          title: Text(_plan.name),
          actions: [
            IconButton(
              tooltip: 'Voir le plan',
              onPressed: _openWorkoutPlan,
              icon: const Icon(Icons.list_alt),
            ),
            TextButton(onPressed: _pause, child: const Text('Pause')),
          ],
        ),
        body: switch (_mode) {
          WorkoutExecutionMode.active => _buildActive(),
          WorkoutExecutionMode.rest => _buildRest(),
          WorkoutExecutionMode.paused => _buildPaused(),
          WorkoutExecutionMode.finished => _buildFinished(),
        },
      ),
    );
  }

  Widget _buildActive() {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _Header(
          elapsedLabel: _formatDuration(
            Duration(
              milliseconds: widget.activeUseCases.elapsedActiveMilliseconds(
                _session,
              ),
            ),
          ),
          progressLabel: _plan.progressLabel(_position),
        ),
        const SizedBox(height: 24),
        _SeriesCounterCard(
          currentSet: _position.setIndex + 1,
          totalSets: _exercise.setsCount,
        ),
        const SizedBox(height: 16),
        Text(_exercise.name, style: Theme.of(context).textTheme.headlineMedium),
        if (_exercise.hasMedia) ...[
          const SizedBox(height: 12),
          Align(
            alignment: Alignment.centerLeft,
            child: Tooltip(
              message: 'Voir les médias de l’exercice',
              child: OutlinedButton.icon(
                onPressed: () => _openExerciseMedia(_exercise),
                icon: const Icon(Icons.perm_media_outlined),
                label: const Text('Voir médias'),
              ),
            ),
          ),
        ],
        const SizedBox(height: 16),
        if (_exercise.steps.isNotEmpty && widget.stepUseCases != null) ...[
          _StepSequencePanel(
            view: _stepProgress,
            exercise: _exercise,
            remainingLabel: _stepRemainingLabel,
            stepScoreController: _stepScoreController,
            stepScoreElapsedLabel: _formatScoreStopwatch(
              Duration(milliseconds: _stepScoreElapsedMs),
            ),
            stepScoreRunning: _stepScoreRunning,
            onStartTimer: _startCurrentStepTimer,
            onCompleteStep: _completeCurrentStep,
            onSkipStep: _skipCurrentStep,
            onSkipPassage: _skipCurrentPassage,
            onSkipSequence: _skipSequence,
            onStartStepScore: _startStepScore,
            onStopStepScore: _stopStepScore,
            onResetStepScore: _resetStepScore,
          ),
          const SizedBox(height: 16),
        ],
        SetMeasureInput(
          exercise: _exercise,
          reps: _reps,
          scoreController: _scoreController,
          onRepsChanged: (value) => setState(() => _reps = value),
        ),
        if (_exercise.stopwatchScoreEnabled) ...[
          const SizedBox(height: 12),
          _ScoreStopwatchInput(
            elapsedLabel: _formatScoreStopwatch(
              Duration(milliseconds: _scoreStopwatchElapsedMs),
            ),
            status: _scoreStopwatch?.status,
            onStart: _startScoreStopwatch,
            onStop: _stopScoreStopwatch,
            onResume: _resumeScoreStopwatch,
            onReset: _resetScoreStopwatch,
            onEdit: _editScoreStopwatchTime,
          ),
        ],
        const SizedBox(height: 24),
        FilledButton(
          onPressed: () => _finishSet(skipped: false),
          style: FilledButton.styleFrom(minimumSize: const Size.fromHeight(56)),
          child: const Text('Terminer la série'),
        ),
        const SizedBox(height: 12),
        OutlinedButton(
          onPressed: () => _finishSet(skipped: true),
          style: OutlinedButton.styleFrom(
            minimumSize: const Size.fromHeight(52),
          ),
          child: Text(_hasActiveStepSequence ? 'Passer la série' : 'Passer'),
        ),
      ],
    );
  }

  Widget _buildRest() {
    final next = _plan.nextPosition(_position);
    final nextExercise = next == null ? null : _plan.exerciseAt(next);
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const Text('Repos avant la prochaine série'),
          const SizedBox(height: 24),
          Center(
            child: Text(
              _formatDuration(Duration(seconds: _remainingRestSeconds)),
              style: AppTextStyles.timer(context),
            ),
          ),
          if (next != null && nextExercise != null) ...[
            const SizedBox(height: 24),
            Text('Ensuite : ${nextExercise.name}', textAlign: TextAlign.center),
            const SizedBox(height: 8),
            Text(
              'Série ${next.setIndex + 1} / ${nextExercise.setsCount}',
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.displaySmall?.copyWith(
                color: Theme.of(context).colorScheme.primary,
                fontSize: 30,
              ),
            ),
            if (nextExercise.hasMedia) ...[
              const SizedBox(height: 12),
              Center(
                child: Tooltip(
                  message: 'Voir les médias de l’exercice',
                  child: OutlinedButton.icon(
                    onPressed: () => _openExerciseMedia(nextExercise),
                    icon: const Icon(Icons.perm_media_outlined),
                    label: const Text('Voir médias'),
                  ),
                ),
              ),
            ],
          ],
          const Spacer(),
          Row(
            children: [
              Expanded(
                child: OutlinedButton(
                  onPressed: () => _adjustRest(-15),
                  child: const Text('-15 s'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: () => _adjustRest(15),
                  child: const Text('+15 s'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          FilledButton(
            onPressed: _skipRest,
            style: FilledButton.styleFrom(
              minimumSize: const Size.fromHeight(52),
            ),
            child: const Text('Ignorer le repos'),
          ),
          const SizedBox(height: 12),
          TextButton(onPressed: _pause, child: const Text('Pause')),
        ],
      ),
    );
  }

  Widget _buildPaused() {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('Pause', style: Theme.of(context).textTheme.headlineMedium),
          const SizedBox(height: 24),
          FilledButton(
            onPressed: _resume,
            style: FilledButton.styleFrom(
              minimumSize: const Size.fromHeight(56),
            ),
            child: const Text('Reprendre'),
          ),
          const SizedBox(height: 12),
          OutlinedButton(
            onPressed: _quitAndSave,
            child: const Text('Quitter et sauvegarder'),
          ),
          const Spacer(),
          TextButton(
            onPressed: _confirmAbandon,
            child: const Text('Abandonner la séance'),
          ),
        ],
      ),
    );
  }

  Widget _buildFinished() {
    final elapsed = Duration(
      milliseconds: widget.activeUseCases.elapsedActiveMilliseconds(_session),
    );
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            'Séance terminée',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const SizedBox(height: 12),
          Text('Temps total : ${_formatDuration(elapsed)}'),
          const SizedBox(height: 24),
          OutlinedButton(
            onPressed: _completedHistory == null ? null : _openHistoryDetail,
            child: const Text('Voir le détail'),
          ),
          OutlinedButton(
            onPressed: _completedHistory == null ? null : _restartCompleted,
            child: const Text('Relancer cette séance'),
          ),
          FilledButton(
            onPressed: () {
              Navigator.of(context).popUntil((route) => route.isFirst);
            },
            child: const Text('Retour à l’accueil'),
          ),
        ],
      ),
    );
  }

  Future<void> _openExerciseMedia(ExecutionExercise exercise) async {
    if (!exercise.hasMedia) {
      return;
    }
    await showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      useSafeArea: true,
      builder: (context) => _ExerciseMediaSheet(
        exercise: exercise,
        restRemainingSeconds: _mode == WorkoutExecutionMode.rest
            ? _remainingRestSeconds
            : null,
        mediaAssetLoader: _loadMediaAsset,
        videoMediaBuilder: widget.videoMediaBuilder,
      ),
    );
  }

  Future<MediaAsset?> _loadMediaAsset(String id) {
    final loader = widget.mediaAssetLoader;
    if (loader != null) return loader(id);
    final mediaUseCases = widget.mediaUseCases;
    if (mediaUseCases == null) return Future<MediaAsset?>.value();
    return mediaUseCases.findById(id);
  }

  int get _scoreStopwatchElapsedMs {
    final manual = _manualScoreTimeMs;
    if (manual != null) {
      return manual;
    }
    final state = _scoreStopwatch;
    if (state == null) {
      return 0;
    }
    return widget.activeUseCases.scoreStopwatchElapsedMilliseconds(state);
  }

  Future<void> _loadScoreStopwatch() async {
    if (!_exercise.stopwatchScoreEnabled) {
      _scoreStopwatch = null;
      _manualScoreTimeMs = null;
      _refreshScoreStopwatchTicker();
      return;
    }
    final state = await widget.activeUseCases.findScoreStopwatch(
      sessionId: _session.metadata.id,
      programIndex: _position.programIndex,
      exerciseIndex: _position.exerciseIndex,
      setIndex: _position.setIndex,
    );
    if (!mounted) return;
    setState(() => _scoreStopwatch = state);
    _refreshScoreStopwatchTicker();
  }

  Future<void> _startScoreStopwatch() async {
    final state = await widget.activeUseCases.startScoreStopwatch(
      sessionId: _session.metadata.id,
      programIndex: _position.programIndex,
      exerciseIndex: _position.exerciseIndex,
      setIndex: _position.setIndex,
    );
    if (!mounted) return;
    setState(() {
      _scoreStopwatch = state;
      _manualScoreTimeMs = null;
    });
    _refreshScoreStopwatchTicker();
  }

  Future<ActiveScoreStopwatchState?> _stopScoreStopwatch() async {
    final state = _scoreStopwatch;
    if (state == null) return null;
    final stopped = await widget.activeUseCases.stopScoreStopwatch(
      sessionId: _session.metadata.id,
      programIndex: _position.programIndex,
      exerciseIndex: _position.exerciseIndex,
      setIndex: _position.setIndex,
    );
    if (!mounted) return stopped;
    setState(() => _scoreStopwatch = stopped);
    _refreshScoreStopwatchTicker();
    return stopped;
  }

  Future<void> _resumeScoreStopwatch() async {
    final state = await widget.activeUseCases.resumeScoreStopwatch(
      sessionId: _session.metadata.id,
      programIndex: _position.programIndex,
      exerciseIndex: _position.exerciseIndex,
      setIndex: _position.setIndex,
    );
    if (!mounted) return;
    setState(() {
      _scoreStopwatch = state;
      _manualScoreTimeMs = null;
    });
    _refreshScoreStopwatchTicker();
  }

  Future<void> _resetScoreStopwatch() async {
    if (_scoreStopwatchElapsedMs > 0) {
      final confirmed = await showDialog<bool>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Réinitialiser le chrono ?'),
          content: const Text('Le temps mesuré sera supprimé.'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(false),
              child: const Text('Annuler'),
            ),
            FilledButton(
              onPressed: () => Navigator.of(context).pop(true),
              child: const Text('Réinitialiser'),
            ),
          ],
        ),
      );
      if (confirmed != true) return;
    }
    await widget.activeUseCases.resetScoreStopwatch(
      sessionId: _session.metadata.id,
      programIndex: _position.programIndex,
      exerciseIndex: _position.exerciseIndex,
      setIndex: _position.setIndex,
    );
    if (!mounted) return;
    setState(() {
      _scoreStopwatch = null;
      _manualScoreTimeMs = null;
    });
    _refreshScoreStopwatchTicker();
  }

  void _refreshScoreStopwatchTicker() {
    final shouldTick =
        _scoreStopwatch?.status == ActiveScoreStopwatchStatus.running;
    if (!shouldTick) {
      _scoreStopwatchTicker?.cancel();
      _scoreStopwatchTicker = null;
      return;
    }
    if (_scoreStopwatchTicker != null) return;
    _scoreStopwatchTicker = Timer.periodic(const Duration(milliseconds: 100), (
      _,
    ) {
      if (mounted) setState(() {});
    });
  }

  Future<void> _editScoreStopwatchTime() async {
    final initial = Duration(milliseconds: _scoreStopwatchElapsedMs);
    final edited = await showDialog<int>(
      context: context,
      builder: (context) => _ScoreStopwatchEditDialog(initial: initial),
    );
    if (edited == null || !mounted) return;
    setState(() => _manualScoreTimeMs = edited);
  }

  String get _stepRemainingLabel {
    final view = _stepProgress;
    final step = view?.currentStep;
    if (view == null || step == null || step.type != ExerciseStepType.time) {
      return '';
    }
    final stepUseCases = widget.stepUseCases;
    if (stepUseCases == null) return '';
    final remainingMs = stepUseCases.remainingMilliseconds(
      state: view.state,
      step: step,
    );
    return _formatStepCountdown(Duration(milliseconds: remainingMs));
  }

  int get _stepScoreElapsedMs {
    if (!_stepScoreRunning || _stepScoreStartedAt == null) {
      return _stepScoreAccumulatedMs;
    }
    return _stepScoreAccumulatedMs +
        DateTime.now().toUtc().difference(_stepScoreStartedAt!).inMilliseconds;
  }

  bool get _hasActiveStepSequence {
    return _exercise.steps.isNotEmpty &&
        widget.stepUseCases != null &&
        _stepProgress?.state.status !=
            ActiveExerciseStepProgressStatus.sequenceComplete;
  }

  Future<void> _loadStepProgress({bool startNextTimed = false}) async {
    final stepUseCases = widget.stepUseCases;
    if (_exercise.steps.isEmpty || stepUseCases == null) {
      _stepTicker?.cancel();
      _stepTicker = null;
      if (!mounted) return;
      setState(() => _stepProgress = null);
      return;
    }
    try {
      final view = await stepUseCases.startOrResumeProgress(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      if (!mounted) return;
      _applyStepProgress(view);
      if (startNextTimed) {
        await _startCurrentStepTimerIfNeeded();
      }
      _refreshStepTicker();
    } on Exception catch (error) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Impossible de charger la séquence : $error')),
      );
    }
  }

  Future<void> _readStepProgress({bool startNextTimed = false}) async {
    final stepUseCases = widget.stepUseCases;
    if (_exercise.steps.isEmpty || stepUseCases == null) return;
    try {
      final view = await stepUseCases.readProgress(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      if (!mounted) return;
      _applyStepProgress(view);
      if (startNextTimed) {
        await _startCurrentStepTimerIfNeeded();
      }
      _refreshStepTicker();
    } on Exception catch (error) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('Impossible de rafraîchir la séquence : $error'),
        ),
      );
    }
  }

  void _applyStepProgress(ActiveExerciseStepProgressView view) {
    final previous = _stepProgress;
    final previousKey = previous == null
        ? null
        : '${previous.state.currentPassageIndex}:${previous.state.currentStepIndex}';
    final nextKey =
        '${view.state.currentPassageIndex}:${view.state.currentStepIndex}';
    if (previousKey != null && previousKey != nextKey) {
      _resetStepScoreInput();
    }
    setState(() => _stepProgress = view);
  }

  void _refreshStepTicker() {
    final view = _stepProgress;
    final running =
        _mode == WorkoutExecutionMode.active &&
        view?.state.status == ActiveExerciseStepProgressStatus.runningTimer;
    if (!running) {
      _stepTicker?.cancel();
      _stepTicker = null;
      _lastCountdownBeepSecond = -1;
      return;
    }
    if (_stepTicker != null) return;
    _stepTicker = Timer.periodic(const Duration(milliseconds: 100), (_) {
      unawaited(_tickStepTimer());
    });
  }

  Future<void> _tickStepTimer() async {
    if (!mounted || _advancingStep) return;
    final view = _stepProgress;
    final step = view?.currentStep;
    if (view == null || step == null || step.type != ExerciseStepType.time) {
      return;
    }
    final stepUseCases = widget.stepUseCases;
    if (stepUseCases == null) return;
    final remainingMs = stepUseCases.remainingMilliseconds(
      state: view.state,
      step: step,
    );
    final remainingSecond = (remainingMs / 1000).ceil();
    if (remainingSecond >= 1 &&
        remainingSecond <= 3 &&
        remainingSecond != _lastCountdownBeepSecond) {
      _lastCountdownBeepSecond = remainingSecond;
      await _playStepAudioCue(short: true);
    }
    if (remainingMs <= 0) {
      _advancingStep = true;
      _stepTicker?.cancel();
      _stepTicker = null;
      await _playStepAudioCue(short: false);
      await _readStepProgress(startNextTimed: true);
      _advancingStep = false;
      return;
    }
    setState(() {});
  }

  Future<void> _playStepAudioCue({required bool short}) async {
    try {
      if (short) {
        await _stepAudioCuePlayer.playShortCountdownBeep();
      } else {
        await _stepAudioCuePlayer.playLongCompletionBeep();
      }
    } on Object catch (error) {
      debugPrint('Audio cue ignored: $error');
    }
  }

  Future<void> _startCurrentStepTimer() async {
    if (_stepProgress?.currentStep?.type != ExerciseStepType.time) return;
    try {
      final stepUseCases = widget.stepUseCases;
      if (stepUseCases == null) return;
      final state = await stepUseCases.startTimer(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      if (!mounted || _stepProgress == null) return;
      setState(() {
        _stepProgress = ActiveExerciseStepProgressView(
          state: state,
          steps: _stepProgress!.steps,
          expectedPassages: _stepProgress!.expectedPassages,
          results: _stepProgress!.results,
        );
      });
      _refreshStepTicker();
    } on Exception catch (error) {
      _showStepError('Impossible de démarrer la séquence : $error');
    }
  }

  Future<void> _startCurrentStepTimerIfNeeded() {
    _refreshStepTicker();
    return Future<void>.value();
  }

  Future<void> _completeCurrentStep() async {
    final view = _stepProgress;
    final step = view?.currentStep;
    if (view == null || step == null) return;
    try {
      final stepUseCases = widget.stepUseCases;
      if (stepUseCases == null) return;
      await stepUseCases.completeCurrentStep(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
        actualReps: step.type == ExerciseStepType.reps
            ? step.defaultTargetValue
            : null,
        actualScore: _actualStepScore(step),
        actualScoreTimeMs: _actualStepScoreTimeMs(step),
      );
      _resetStepScoreInput();
      await _readStepProgress(startNextTimed: true);
    } on Exception catch (error) {
      _showStepError('Impossible de valider l’étape : $error');
    }
  }

  Future<void> _skipCurrentStep() async {
    final confirmed = await _confirmStepSkip(
      'Passer cette étape ?',
      _stepProgress?.state.status ==
              ActiveExerciseStepProgressStatus.runningTimer
          ? 'Le chrono de cette étape sera arrêté.'
          : 'Cette étape sera marquée comme passée.',
      'Passer l’étape',
    );
    if (confirmed != true) return;
    try {
      final stepUseCases = widget.stepUseCases;
      if (stepUseCases == null) return;
      await stepUseCases.skipCurrentStep(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      _resetStepScoreInput();
      await _readStepProgress(startNextTimed: true);
    } on Exception catch (error) {
      _showStepError('Impossible de passer l’étape : $error');
    }
  }

  Future<void> _skipCurrentPassage() async {
    final confirmed = await _confirmStepSkip(
      'Passer ce passage ?',
      'Les étapes restantes de ce passage seront marquées comme passées.',
      'Passer ce passage',
    );
    if (confirmed != true) return;
    try {
      final stepUseCases = widget.stepUseCases;
      if (stepUseCases == null) return;
      await stepUseCases.skipCurrentPassage(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      _resetStepScoreInput();
      await _readStepProgress(startNextTimed: true);
    } on Exception catch (error) {
      _showStepError('Impossible de passer ce passage : $error');
    }
  }

  Future<void> _skipSequence() async {
    final confirmed = await _confirmStepSkip(
      'Passer la séquence ?',
      'Toutes les étapes restantes seront marquées comme passées.',
      'Passer la séquence',
    );
    if (confirmed != true) return;
    try {
      final stepUseCases = widget.stepUseCases;
      if (stepUseCases == null) return;
      await stepUseCases.skipSequence(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      _resetStepScoreInput();
      await _readStepProgress();
    } on Exception catch (error) {
      _showStepError('Impossible de passer la séquence : $error');
    }
  }

  Future<bool?> _confirmStepSkip(
    String title,
    String content,
    String actionLabel,
  ) {
    return showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('Annuler'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: Text(actionLabel),
          ),
        ],
      ),
    );
  }

  double? _actualStepScore(ExerciseStep step) {
    if (!step.hasScore || step.scoreInputMode != ScoreInputMode.manual) {
      return null;
    }
    return double.tryParse(_stepScoreController.text.trim());
  }

  int? _actualStepScoreTimeMs(ExerciseStep step) {
    if (!step.hasScore || step.scoreInputMode != ScoreInputMode.stopwatch) {
      return null;
    }
    final elapsed = _stepScoreElapsedMs;
    return elapsed > 0 ? elapsed : null;
  }

  void _startStepScore() {
    if (_stepScoreRunning) return;
    setState(() {
      _stepScoreRunning = true;
      _stepScoreStartedAt = DateTime.now().toUtc();
    });
    _stepScoreTicker ??= Timer.periodic(const Duration(milliseconds: 100), (_) {
      if (mounted) setState(() {});
    });
  }

  void _stopStepScore() {
    if (!_stepScoreRunning || _stepScoreStartedAt == null) return;
    setState(() {
      _stepScoreAccumulatedMs = _stepScoreElapsedMs;
      _stepScoreStartedAt = null;
      _stepScoreRunning = false;
    });
    _stepScoreTicker?.cancel();
    _stepScoreTicker = null;
  }

  void _resetStepScore() {
    setState(_resetStepScoreInput);
  }

  void _resetStepScoreInput() {
    _stepScoreController.clear();
    _stepScoreStartedAt = null;
    _stepScoreAccumulatedMs = 0;
    _stepScoreRunning = false;
    _stepScoreTicker?.cancel();
    _stepScoreTicker = null;
  }

  void _showStepError(String message) {
    if (!mounted) return;
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text(message)));
  }

  bool get _canPauseFromNavigation {
    return _mode == WorkoutExecutionMode.active ||
        _mode == WorkoutExecutionMode.rest;
  }

  void _handleSystemBack(bool didPop) {
    if (didPop || !_canPauseFromNavigation) return;
    unawaited(_pause());
  }

  Future<void> _pause() async {
    _restTicker?.cancel();
    _stepTicker?.cancel();
    _stepTicker = null;
    if (_stepScoreRunning) {
      _stopStepScore();
    }
    if (_scoreStopwatch?.status == ActiveScoreStopwatchStatus.running) {
      await _stopScoreStopwatch();
    }
    _session = await widget.activeUseCases.pause(_session.metadata.id);
    if (!mounted) return;
    setState(() => _mode = WorkoutExecutionMode.paused);
  }

  Future<void> _resume() async {
    _session = await widget.activeUseCases.resume(_session.metadata.id);
    if (!mounted) return;
    final restoredRest = await _restoreActiveRest();
    if (!mounted || restoredRest) return;
    await _loadScoreStopwatch();
    await _loadStepProgress();
    if (_stepProgress?.state.status ==
        ActiveExerciseStepProgressStatus.pausedTimer) {
      await _startCurrentStepTimer();
    }
    if (!mounted) return;
    setState(() => _mode = WorkoutExecutionMode.active);
    _refreshStepTicker();
  }

  Future<void> _quitAndSave() async {
    await widget.activeUseCases.quitAndSave(_session.metadata.id);
    if (!mounted) return;
    Navigator.of(context).popUntil((route) => route.isFirst);
  }

  Future<void> _confirmAbandon() async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Abandonner la séance ?'),
        content: const Text('Cette action est définitive.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('Annuler'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: const Text('Abandonner'),
          ),
        ],
      ),
    );
    if (confirmed != true) return;
    await widget.activeUseCases.abandon(_session.metadata.id);
    if (!mounted) return;
    Navigator.of(context).popUntil((route) => route.isFirst);
  }

  Future<void> _openWorkoutPlan() async {
    final setStates = await widget.activeUseCases.listSetResults(
      _session.metadata.id,
    );
    final activeRest = await widget.activeUseCases.findActiveRest(
      sessionId: _session.metadata.id,
    );
    if (!mounted) return;
    await showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      useSafeArea: true,
      builder: (context) => FractionallySizedBox(
        heightFactor: 1,
        child: _WorkoutPlanSheet(
          sessionId: _session.metadata.id,
          plan: _plan,
          currentPosition: _position,
          setStates: setStates,
          activeRest: activeRest,
          activeUseCases: widget.activeUseCases,
          stepUseCases: widget.stepUseCases,
        ),
      ),
    );
  }

  Future<void> _finishSet({required bool skipped}) async {
    if (skipped && _hasActiveStepSequence) {
      final confirmed = await _confirmStepSkip(
        'Passer la série ?',
        'La séquence en cours sera arrêtée.',
        'Passer la série',
      );
      if (confirmed != true) return;
      final stepUseCases = widget.stepUseCases;
      if (stepUseCases == null) return;
      await stepUseCases.skipSequence(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      _resetStepScoreInput();
      await _readStepProgress();
    }
    if (skipped &&
        _scoreStopwatch?.status == ActiveScoreStopwatchStatus.running) {
      final confirmed = await showDialog<bool>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Passer cette série ?'),
          content: const Text('Le chrono en cours sera ignoré.'),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(false),
              child: const Text('Continuer la série'),
            ),
            FilledButton(
              onPressed: () => Navigator.of(context).pop(true),
              child: const Text('Passer'),
            ),
          ],
        ),
      );
      if (confirmed != true) return;
      await widget.activeUseCases.resetScoreStopwatch(
        sessionId: _session.metadata.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
      );
      if (!mounted) return;
      setState(() {
        _scoreStopwatch = null;
        _manualScoreTimeMs = null;
      });
      _refreshScoreStopwatchTicker();
    }
    if (!skipped &&
        _exercise.stopwatchScoreEnabled &&
        _scoreStopwatch == null &&
        _manualScoreTimeMs == null) {
      final action = await showDialog<_MissingStopwatchAction>(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Aucun temps chronométré'),
          content: const Text("Tu n'as pas démarré le chrono score."),
          actions: [
            TextButton(
              onPressed: () =>
                  Navigator.of(context).pop(_MissingStopwatchAction.start),
              child: const Text('Démarrer le chrono'),
            ),
            FilledButton(
              onPressed: () => Navigator.of(
                context,
              ).pop(_MissingStopwatchAction.finishWithout),
              child: const Text('Terminer sans chrono'),
            ),
          ],
        ),
      );
      if (action == _MissingStopwatchAction.start) {
        await _startScoreStopwatch();
        return;
      }
      if (action != _MissingStopwatchAction.finishWithout) {
        return;
      }
    }
    await _recordAndAdvance(skipped: skipped);
  }

  Future<void> _recordAndAdvance({required bool skipped}) async {
    try {
      final score = double.tryParse(_scoreController.text.trim());
      var actualScoreTimeMs = skipped ? null : _manualScoreTimeMs;
      if (!skipped &&
          _exercise.stopwatchScoreEnabled &&
          actualScoreTimeMs == null) {
        final state = _scoreStopwatch;
        if (state?.status == ActiveScoreStopwatchStatus.running) {
          final stopped = await _stopScoreStopwatch();
          actualScoreTimeMs = stopped?.accumulatedMs;
        } else {
          actualScoreTimeMs = state?.accumulatedMs;
        }
      }
      final actualTimeMs = DateTime.now()
          .toUtc()
          .difference(_seriesStartedAt)
          .inMilliseconds;
      await widget.activeUseCases.recordCurrentSetResult(
        sessionId: _session.metadata.id,
        programSnapshotId: _plan.programAt(_position).id,
        exerciseSnapshotId: _exercise.id,
        programIndex: _position.programIndex,
        exerciseIndex: _position.exerciseIndex,
        setIndex: _position.setIndex,
        actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null),
        actualScore: skipped
            ? null
            : (_exercise.manualScoreEnabled ? score : null),
        actualScoreTimeMs: actualScoreTimeMs,
        scoreInputModeSnapshot: _exercise.scoreInputMode,
        scoreLabelSnapshot: _exercise.scoreLabel,
        scoreUnitSnapshot: _exercise.scoreUnit,
        actualTimeMs: skipped
            ? null
            : (_exercise.timeEnabled ? actualTimeMs : null),
      );

      final next = _plan.nextPosition(_position);
      if (next == null) {
        await _complete();
        return;
      }
      final shouldRest = _plan.shouldShowRestAfter(_position);
      if (shouldRest) {
        final rest = await widget.activeUseCases.startRestAfterSet(
          sessionId: _session.metadata.id,
          afterProgramIndex: _position.programIndex,
          afterExerciseIndex: _position.exerciseIndex,
          afterSetIndex: _position.setIndex,
          plannedRestSeconds: _exercise.restSeconds,
        );
        if (!mounted) return;
        _activeRestStateId = rest.metadata.id;
        _remainingRestSeconds = _exercise.restSeconds;
        _startRestTicker(next);
        setState(() => _mode = WorkoutExecutionMode.rest);
        return;
      }
      await _moveTo(next);
    } on Exception catch (error) {
      if (!mounted) return;
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Impossible de terminer la série : $error')),
      );
      return;
    }
  }

  void _startRestTicker(ExecutionPosition next) {
    _restTicker?.cancel();
    _restTicker = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (!mounted) return;
      if (_remainingRestSeconds <= 1) {
        timer.cancel();
        unawaited(_finishRestAndMove(next));
      } else {
        setState(() => _remainingRestSeconds -= 1);
      }
    });
  }

  Future<bool> _restoreActiveRest() async {
    final rest = await widget.activeUseCases.findActiveRest(
      sessionId: _session.metadata.id,
    );
    if (!mounted || rest == null) {
      return false;
    }
    final after = ExecutionPosition(
      programIndex: rest.afterProgramIndex,
      exerciseIndex: rest.afterExerciseIndex,
      setIndex: rest.afterSetIndex,
    );
    final next = _plan.nextPosition(after);
    final remaining = _remainingSecondsFor(rest);
    _activeRestStateId = rest.metadata.id;
    if (next == null || remaining <= 0) {
      await _finishRestAndMove(next);
      return true;
    }
    _remainingRestSeconds = remaining;
    _startRestTicker(next);
    if (!mounted) return true;
    setState(() => _mode = WorkoutExecutionMode.rest);
    return true;
  }

  int _remainingSecondsFor(ActiveRestState rest) {
    final elapsedSeconds = DateTime.now()
        .toUtc()
        .difference(rest.startedAt.toUtc())
        .inSeconds;
    return (rest.adjustedRestSeconds - elapsedSeconds).clamp(0, 9999).toInt();
  }

  Future<void> _adjustRest(int deltaSeconds) async {
    final restStateId = _activeRestStateId;
    setState(() {
      _remainingRestSeconds = (_remainingRestSeconds + deltaSeconds)
          .clamp(0, 9999)
          .toInt();
    });
    if (restStateId == null) return;
    await widget.activeUseCases.adjustRestSeconds(
      restStateId: restStateId,
      deltaSeconds: deltaSeconds,
    );
  }

  Future<void> _skipRest() async {
    _restTicker?.cancel();
    final next = _plan.nextPosition(_position);
    await _finishRestAndMove(next);
  }

  Future<void> _finishRestAndMove(ExecutionPosition? next) async {
    final restStateId = _activeRestStateId;
    _activeRestStateId = null;
    if (restStateId != null) {
      await widget.activeUseCases.skipRest(restStateId: restStateId);
    }
    if (next == null) {
      await _complete();
      return;
    }
    await _moveTo(next);
  }

  Future<void> _moveTo(ExecutionPosition position) async {
    _scoreController.clear();
    _reps = _initialRepsFor(_plan.exerciseAt(position));
    _activeRestStateId = null;
    _scoreStopwatch = null;
    _manualScoreTimeMs = null;
    _stepProgress = null;
    _resetStepScoreInput();
    _refreshScoreStopwatchTicker();
    _refreshStepTicker();
    _seriesStartedAt = DateTime.now().toUtc();
    _session = await widget.activeUseCases.updateProgress(
      sessionId: _session.metadata.id,
      programIndex: position.programIndex,
      exerciseIndex: position.exerciseIndex,
      setIndex: position.setIndex,
    );
    if (!mounted) return;
    await _loadScoreStopwatch();
    await _loadStepProgress();
    if (!mounted) return;
    setState(() => _mode = WorkoutExecutionMode.active);
  }

  Future<void> _complete() async {
    _session = await widget.activeUseCases.complete(_session.metadata.id);
    _completedHistory = await widget.closeUseCase.close(
      sessionId: _session.metadata.id,
      nameSnapshot: _plan.name,
      completed: true,
    );
    if (!mounted) return;
    setState(() => _mode = WorkoutExecutionMode.finished);
  }

  Future<void> _openHistoryDetail() async {
    final history = _completedHistory;
    if (history == null) return;
    await Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => HistoryDetailScreen(
          history: history,
          historyUseCases: widget.historyUseCases,
          workoutTemplateUseCases: widget.workoutTemplateUseCases,
          activeUseCases: widget.activeUseCases,
          stepUseCases: widget.stepUseCases,
          closeUseCase: widget.closeUseCase,
        ),
      ),
    );
  }

  Future<void> _restartCompleted() async {
    final history = _completedHistory;
    if (history == null) return;
    ActiveWorkoutSession session;
    final sourceId = history.sourceWorkoutTemplateId;
    if (sourceId != null &&
        await widget.workoutTemplateUseCases.findById(sourceId) != null) {
      session = await widget.activeUseCases.startFromTemplate(sourceId);
    } else {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text(
              "La séance originale n'existe plus. Une copie va être utilisée.",
            ),
          ),
        );
      }
      session = await widget.activeUseCases.startFromHistory(history);
    }
    if (!mounted) return;
    await Navigator.of(context).pushReplacement(
      MaterialPageRoute(
        builder: (context) => WorkoutExecutionScreen(
          initialSession: session,
          activeUseCases: widget.activeUseCases,
          stepUseCases: widget.stepUseCases,
          closeUseCase: widget.closeUseCase,
          historyUseCases: widget.historyUseCases,
          workoutTemplateUseCases: widget.workoutTemplateUseCases,
          mediaUseCases: widget.mediaUseCases,
          mediaAssetLoader: widget.mediaAssetLoader,
          videoMediaBuilder: widget.videoMediaBuilder,
        ),
      ),
    );
  }

  int _initialRepsFor(ExecutionExercise exercise) {
    if (!exercise.repsEnabled) return 0;
    return exercise.targetReps ?? 0;
  }
}

final class SetMeasureInput extends StatelessWidget {
  const SetMeasureInput({
    required this.exercise,
    required this.reps,
    required this.scoreController,
    required this.onRepsChanged,
    super.key,
  });

  final ExecutionExercise exercise;
  final int reps;
  final TextEditingController scoreController;
  final ValueChanged<int> onRepsChanged;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        if (exercise.timeEnabled)
          Card(
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: Column(
                children: [
                  const Text('Temps'),
                  const SizedBox(height: 8),
                  Text(
                    exercise.targetTimeSeconds == null
                        ? 'Chronométrer'
                        : '${exercise.targetTimeSeconds} s',
                    style: AppTextStyles.scoreNumber(context),
                  ),
                ],
              ),
            ),
          ),
        if (exercise.repsEnabled) ...[
          const SizedBox(height: 12),
          Row(
            children: [
              const Expanded(child: Text('Répétitions')),
              IconButton(
                onPressed: () =>
                    onRepsChanged((reps - 1).clamp(0, 999).toInt()),
                icon: const Icon(Icons.remove),
              ),
              Text('$reps', style: AppTextStyles.scoreNumber(context)),
              IconButton(
                onPressed: () => onRepsChanged(reps + 1),
                icon: const Icon(Icons.add),
              ),
            ],
          ),
        ],
        if (exercise.manualScoreEnabled) ...[
          const SizedBox(height: 12),
          TextField(
            controller: scoreController,
            style: AppTextStyles.scoreNumber(context),
            decoration: InputDecoration(
              labelText: exercise.scoreUnit == null
                  ? 'Score'
                  : 'Score (${exercise.scoreUnit})',
            ),
            keyboardType: TextInputType.number,
          ),
        ],
      ],
    );
  }
}

enum _MissingStopwatchAction { start, finishWithout }

final class _StepSequencePanel extends StatelessWidget {
  const _StepSequencePanel({
    required this.view,
    required this.exercise,
    required this.remainingLabel,
    required this.stepScoreController,
    required this.stepScoreElapsedLabel,
    required this.stepScoreRunning,
    required this.onStartTimer,
    required this.onCompleteStep,
    required this.onSkipStep,
    required this.onSkipPassage,
    required this.onSkipSequence,
    required this.onStartStepScore,
    required this.onStopStepScore,
    required this.onResetStepScore,
  });

  final ActiveExerciseStepProgressView? view;
  final ExecutionExercise exercise;
  final String remainingLabel;
  final TextEditingController stepScoreController;
  final String stepScoreElapsedLabel;
  final bool stepScoreRunning;
  final VoidCallback onStartTimer;
  final VoidCallback onCompleteStep;
  final VoidCallback onSkipStep;
  final VoidCallback onSkipPassage;
  final VoidCallback onSkipSequence;
  final VoidCallback onStartStepScore;
  final VoidCallback onStopStepScore;
  final VoidCallback onResetStepScore;

  @override
  Widget build(BuildContext context) {
    final view = this.view;
    if (view == null) {
      return const CourtBlazerAccentPanel(
        padding: EdgeInsets.all(16),
        child: Center(child: CircularProgressIndicator()),
      );
    }
    final currentStep = view.currentStep;
    final sequenceComplete =
        view.state.status == ActiveExerciseStepProgressStatus.sequenceComplete;
    final completedPassages = sequenceComplete
        ? view.expectedPassages
        : view.state.currentPassageIndex;
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('SÉQUENCE', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 8),
          Text(
            view.expectedPassages > 1 && !sequenceComplete
                ? 'Passage ${view.state.currentPassageIndex + 1} / ${view.expectedPassages}'
                : view.expectedPassages > 1
                ? 'Séquence terminée'
                : 'Passage en cours',
          ),
          const SizedBox(height: 12),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Row(
              children: [
                for (var index = 0; index < view.steps.length; index++) ...[
                  _StepProgressChip(
                    index: index,
                    status: _stepDisplayStatus(view, index),
                  ),
                  if (index < view.steps.length - 1) const SizedBox(width: 8),
                ],
              ],
            ),
          ),
          const SizedBox(height: 16),
          if (sequenceComplete || currentStep == null)
            _SequenceCompleteSummary(
              completedPassages: completedPassages,
              expectedPassages: view.expectedPassages,
            )
          else ...[
            Text(
              'ÉTAPE ${view.state.currentStepIndex + 1} / ${view.steps.length}',
              style: Theme.of(context).textTheme.labelLarge,
            ),
            const SizedBox(height: 4),
            Text(
              currentStep.name,
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 12),
            if (currentStep.type == ExerciseStepType.time)
              _TimedStepBody(
                step: currentStep,
                remainingLabel: remainingLabel,
                running:
                    view.state.status ==
                    ActiveExerciseStepProgressStatus.runningTimer,
                readyToStart: _isNextTimedStepReady(view),
                onStartTimer: onStartTimer,
              )
            else
              _RepsStepBody(step: currentStep, onCompleteStep: onCompleteStep),
            if (currentStep.hasScore) ...[
              const SizedBox(height: 16),
              _StepScoreInput(
                step: currentStep,
                controller: stepScoreController,
                elapsedLabel: stepScoreElapsedLabel,
                running: stepScoreRunning,
                onStart: onStartStepScore,
                onStop: onStopStepScore,
                onReset: onResetStepScore,
              ),
            ],
            const SizedBox(height: 12),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                OutlinedButton(
                  onPressed: onSkipStep,
                  child: const Text('Passer l’étape'),
                ),
                TextButton(
                  onPressed: onSkipPassage,
                  child: const Text('Passer ce passage'),
                ),
                TextButton(
                  onPressed: onSkipSequence,
                  child: const Text('Passer la séquence'),
                ),
              ],
            ),
          ],
          if (exercise.repsEnabled) ...[
            const SizedBox(height: 16),
            Text(
              'Passages réalisés : $completedPassages / ${view.expectedPassages}',
            ),
          ],
        ],
      ),
    );
  }
}

final class _TimedStepBody extends StatelessWidget {
  const _TimedStepBody({
    required this.step,
    required this.remainingLabel,
    required this.running,
    required this.readyToStart,
    required this.onStartTimer,
  });

  final ExerciseStep step;
  final String remainingLabel;
  final bool running;
  final bool readyToStart;
  final VoidCallback onStartTimer;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text('Objectif : ${step.defaultTargetValue} s'),
        const SizedBox(height: 8),
        Center(
          child: Text(
            remainingLabel,
            style: AppTextStyles.timer(
              context,
            ).copyWith(color: Theme.of(context).colorScheme.primary),
          ),
        ),
        if (!running) ...[
          const SizedBox(height: 12),
          if (readyToStart) ...[
            Text(
              'Chrono suivant prêt',
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.titleMedium?.copyWith(
                color: Theme.of(context).colorScheme.primary,
              ),
            ),
            const SizedBox(height: 8),
          ],
          FilledButton.icon(
            onPressed: onStartTimer,
            icon: const Icon(Icons.play_arrow),
            label: Text(
              readyToStart ? 'Démarrer le chrono' : 'Démarrer la séquence',
            ),
          ),
        ],
      ],
    );
  }
}

bool _isNextTimedStepReady(ActiveExerciseStepProgressView view) {
  final step = view.currentStep;
  if (step == null ||
      step.type != ExerciseStepType.time ||
      view.state.status != ActiveExerciseStepProgressStatus.stoppedTimer) {
    return false;
  }
  return view.results.any((result) {
    if (result.passageIndex < view.state.currentPassageIndex) {
      return true;
    }
    return result.passageIndex == view.state.currentPassageIndex &&
        result.stepIndex < view.state.currentStepIndex;
  });
}

final class _RepsStepBody extends StatelessWidget {
  const _RepsStepBody({required this.step, required this.onCompleteStep});

  final ExerciseStep step;
  final VoidCallback onCompleteStep;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          '${step.defaultTargetValue}',
          style: AppTextStyles.timer(
            context,
          ).copyWith(color: Theme.of(context).colorScheme.primary),
        ),
        Text('RÉPÉTITIONS', style: Theme.of(context).textTheme.labelLarge),
        const SizedBox(height: 12),
        FilledButton.icon(
          onPressed: onCompleteStep,
          icon: const Icon(Icons.check),
          label: const Text('Étape suivante'),
        ),
      ],
    );
  }
}

final class _StepScoreInput extends StatelessWidget {
  const _StepScoreInput({
    required this.step,
    required this.controller,
    required this.elapsedLabel,
    required this.running,
    required this.onStart,
    required this.onStop,
    required this.onReset,
  });

  final ExerciseStep step;
  final TextEditingController controller;
  final String elapsedLabel;
  final bool running;
  final VoidCallback onStart;
  final VoidCallback onStop;
  final VoidCallback onReset;

  @override
  Widget build(BuildContext context) {
    if (step.scoreInputMode == ScoreInputMode.stopwatch) {
      return Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text(
            'Chrono score d’étape',
            style: Theme.of(context).textTheme.titleSmall,
          ),
          const SizedBox(height: 8),
          Center(
            child: Text(
              elapsedLabel,
              style: AppTextStyles.scoreNumber(context),
            ),
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            alignment: WrapAlignment.center,
            children: [
              FilledButton.icon(
                onPressed: running ? onStop : onStart,
                icon: Icon(running ? Icons.stop : Icons.play_arrow),
                label: Text(running ? 'Arrêter' : 'Démarrer'),
              ),
              OutlinedButton.icon(
                onPressed: onReset,
                icon: const Icon(Icons.restart_alt),
                label: const Text('Réinitialiser'),
              ),
            ],
          ),
        ],
      );
    }
    final unit = step.scoreUnit?.trim();
    return TextField(
      controller: controller,
      decoration: InputDecoration(
        labelText: unit == null || unit.isEmpty
            ? 'Score de l’étape'
            : 'Score de l’étape ($unit)',
      ),
      keyboardType: TextInputType.number,
    );
  }
}

final class _SequenceCompleteSummary extends StatelessWidget {
  const _SequenceCompleteSummary({
    required this.completedPassages,
    required this.expectedPassages,
  });

  final int completedPassages;
  final int expectedPassages;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Text(
          'Séquence terminée',
          style: Theme.of(context).textTheme.headlineSmall,
        ),
        const SizedBox(height: 8),
        Text('$completedPassages / $expectedPassages passages réalisés'),
      ],
    );
  }
}

final class _StepProgressChip extends StatelessWidget {
  const _StepProgressChip({required this.index, required this.status});

  final int index;
  final _StepDisplayStatus status;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final tokens = courtBlazerTokensOf(context);
    final background = switch (status) {
      _StepDisplayStatus.current => theme.colorScheme.primary,
      _StepDisplayStatus.completed => tokens.success.withAlpha(35),
      _StepDisplayStatus.skipped => theme.colorScheme.surface,
      _StepDisplayStatus.todo => theme.colorScheme.surface,
    };
    final border = switch (status) {
      _StepDisplayStatus.current => theme.colorScheme.primary,
      _StepDisplayStatus.completed => tokens.success,
      _StepDisplayStatus.skipped => tokens.mutedText,
      _StepDisplayStatus.todo => tokens.border,
    };
    final foreground = status == _StepDisplayStatus.current
        ? theme.colorScheme.onPrimary
        : theme.colorScheme.onSurface;
    return Container(
      width: 36,
      height: 36,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: background,
        borderRadius: BorderRadius.circular(6),
        border: Border.all(color: border),
      ),
      child: Text(
        '${index + 1}',
        style: theme.textTheme.labelLarge?.copyWith(color: foreground),
      ),
    );
  }
}

enum _StepDisplayStatus { completed, skipped, current, todo }

_StepDisplayStatus _stepDisplayStatus(
  ActiveExerciseStepProgressView view,
  int stepIndex,
) {
  for (final result in view.results) {
    if (result.passageIndex == view.state.currentPassageIndex &&
        result.stepIndex == stepIndex) {
      return result.status == SetResultStatus.completed
          ? _StepDisplayStatus.completed
          : _StepDisplayStatus.skipped;
    }
  }
  if (view.state.status != ActiveExerciseStepProgressStatus.sequenceComplete &&
      stepIndex == view.state.currentStepIndex) {
    return _StepDisplayStatus.current;
  }
  if (stepIndex < view.state.currentStepIndex) {
    return _StepDisplayStatus.completed;
  }
  return _StepDisplayStatus.todo;
}

final class _ScoreStopwatchInput extends StatelessWidget {
  const _ScoreStopwatchInput({
    required this.elapsedLabel,
    required this.status,
    required this.onStart,
    required this.onStop,
    required this.onResume,
    required this.onReset,
    required this.onEdit,
  });

  final String elapsedLabel;
  final ActiveScoreStopwatchStatus? status;
  final VoidCallback onStart;
  final VoidCallback onStop;
  final VoidCallback onResume;
  final VoidCallback onReset;
  final VoidCallback onEdit;

  @override
  Widget build(BuildContext context) {
    final running = status == ActiveScoreStopwatchStatus.running;
    final stopped = status == ActiveScoreStopwatchStatus.stopped;
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('Chrono score', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 12),
          Center(
            child: Text(
              elapsedLabel,
              style: AppTextStyles.scoreNumber(context),
            ),
          ),
          const SizedBox(height: 12),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            alignment: WrapAlignment.center,
            children: [
              if (status == null)
                FilledButton.icon(
                  onPressed: onStart,
                  icon: const Icon(Icons.play_arrow),
                  label: const Text('Démarrer'),
                )
              else if (running)
                FilledButton.icon(
                  onPressed: onStop,
                  icon: const Icon(Icons.stop),
                  label: const Text('Arrêter'),
                )
              else ...[
                FilledButton.icon(
                  onPressed: onResume,
                  icon: const Icon(Icons.play_arrow),
                  label: const Text('Reprendre'),
                ),
                OutlinedButton.icon(
                  onPressed: onReset,
                  icon: const Icon(Icons.restart_alt),
                  label: const Text('Réinitialiser'),
                ),
              ],
              if (stopped)
                TextButton.icon(
                  onPressed: onEdit,
                  icon: const Icon(Icons.edit),
                  label: const Text('Modifier le temps'),
                ),
            ],
          ),
        ],
      ),
    );
  }
}

final class _ScoreStopwatchEditDialog extends StatefulWidget {
  const _ScoreStopwatchEditDialog({required this.initial});

  final Duration initial;

  @override
  State<_ScoreStopwatchEditDialog> createState() =>
      _ScoreStopwatchEditDialogState();
}

final class _ScoreStopwatchEditDialogState
    extends State<_ScoreStopwatchEditDialog> {
  late final TextEditingController _minutesController;
  late final TextEditingController _secondsController;
  late final TextEditingController _tenthsController;

  @override
  void initState() {
    super.initState();
    final totalTenths = (widget.initial.inMilliseconds / 100).round();
    final minutes = totalTenths ~/ 600;
    final seconds = (totalTenths ~/ 10) % 60;
    final tenths = totalTenths % 10;
    _minutesController = TextEditingController(text: '$minutes');
    _secondsController = TextEditingController(text: '$seconds');
    _tenthsController = TextEditingController(text: '$tenths');
  }

  @override
  void dispose() {
    _minutesController.dispose();
    _secondsController.dispose();
    _tenthsController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Modifier le temps'),
      content: Row(
        children: [
          Expanded(
            child: TextField(
              controller: _minutesController,
              decoration: const InputDecoration(labelText: 'Min'),
              keyboardType: TextInputType.number,
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: TextField(
              controller: _secondsController,
              decoration: const InputDecoration(labelText: 'S'),
              keyboardType: TextInputType.number,
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: TextField(
              controller: _tenthsController,
              decoration: const InputDecoration(labelText: 'Dixièmes'),
              keyboardType: TextInputType.number,
            ),
          ),
        ],
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Annuler'),
        ),
        FilledButton(
          onPressed: () {
            final minutes = int.tryParse(_minutesController.text.trim()) ?? 0;
            final seconds = int.tryParse(_secondsController.text.trim()) ?? 0;
            final tenths = int.tryParse(_tenthsController.text.trim()) ?? 0;
            final milliseconds =
                (minutes * 60 * 1000) + (seconds * 1000) + (tenths * 100);
            Navigator.of(context).pop(milliseconds);
          },
          child: const Text('Enregistrer'),
        ),
      ],
    );
  }
}

final class _WorkoutPlanSheet extends StatefulWidget {
  const _WorkoutPlanSheet({
    required this.sessionId,
    required this.plan,
    required this.currentPosition,
    required this.setStates,
    required this.activeUseCases,
    this.stepUseCases,
    this.activeRest,
  });

  final String sessionId;
  final WorkoutExecutionPlan plan;
  final ExecutionPosition currentPosition;
  final List<SetResultPositionState> setStates;
  final ActiveWorkoutSessionUseCases activeUseCases;
  final ActiveExerciseStepUseCases? stepUseCases;
  final ActiveRestState? activeRest;

  @override
  State<_WorkoutPlanSheet> createState() => _WorkoutPlanSheetState();
}

final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
  Timer? _timer;
  late int _remainingRestSeconds;
  late List<SetResultPositionState> _setStates;

  @override
  void initState() {
    super.initState();
    _setStates = widget.setStates;
    _remainingRestSeconds = _remainingSecondsForRest(widget.activeRest);
    if (widget.activeRest != null && _remainingRestSeconds > 0) {
      _timer = Timer.periodic(const Duration(seconds: 1), (_) {
        if (!mounted) return;
        setState(() {
          _remainingRestSeconds = (_remainingRestSeconds - 1)
              .clamp(0, 9999)
              .toInt();
        });
      });
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final statesByPosition = {
      for (final state in _setStates) _stateKey(state): state,
    };
    return Scaffold(
      appBar: AppBar(
        title: const Text('Plan de séance'),
        leading: IconButton(
          tooltip: 'Fermer',
          onPressed: () => Navigator.of(context).pop(),
          icon: const Icon(Icons.close),
        ),
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          const Text('Touchez une série pour voir ses valeurs.'),
          if (widget.activeRest != null) ...[
            const SizedBox(height: 12),
            _RestPlanBanner(remainingSeconds: _remainingRestSeconds),
          ],
          const SizedBox(height: 24),
          for (
            var programIndex = 0;
            programIndex < widget.plan.programs.length;
            programIndex++
          ) ...[
            Text(
              widget.plan.programs[programIndex].name,
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            for (
              var exerciseIndex = 0;
              exerciseIndex <
                  widget.plan.programs[programIndex].exercises.length;
              exerciseIndex++
            ) ...[
              Text(
                widget
                    .plan
                    .programs[programIndex]
                    .exercises[exerciseIndex]
                    .name,
                style: Theme.of(context).textTheme.titleSmall,
              ),
              const SizedBox(height: 4),
              for (
                var setIndex = 0;
                setIndex <
                    widget
                        .plan
                        .programs[programIndex]
                        .exercises[exerciseIndex]
                        .setsCount;
                setIndex++
              )
                _WorkoutPlanSetTile(
                  position: ExecutionPosition(
                    programIndex: programIndex,
                    exerciseIndex: exerciseIndex,
                    setIndex: setIndex,
                  ),
                  currentPosition: widget.currentPosition,
                  exercise: widget
                      .plan
                      .programs[programIndex]
                      .exercises[exerciseIndex],
                  state:
                      statesByPosition[_positionKey(
                        programIndex,
                        exerciseIndex,
                        setIndex,
                      )],
                  onUpdated: _refreshAfterEdit,
                  activeUseCases: widget.activeUseCases,
                  stepUseCases: widget.stepUseCases,
                  sessionId: widget.sessionId,
                  activeRest: widget.activeRest,
                ),
              const SizedBox(height: 16),
            ],
          ],
        ],
      ),
    );
  }

  Future<void> _refreshAfterEdit() async {
    final updated = await widget.activeUseCases.listSetResults(
      widget.sessionId,
    );
    if (!mounted) return;
    setState(() => _setStates = updated);
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(const SnackBar(content: Text('Série mise à jour')));
  }
}

final class _RestPlanBanner extends StatelessWidget {
  const _RestPlanBanner({required this.remainingSeconds});

  final int remainingSeconds;

  @override
  Widget build(BuildContext context) {
    final label = remainingSeconds <= 0
        ? 'Repos terminé · Reprendre'
        : 'Repos en cours · ${_formatDuration(Duration(seconds: remainingSeconds))}';
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.all(12),
      child: Text(label),
    );
  }
}

enum _MediaSheetSection { images, video }

final class _ExerciseMediaSheet extends StatefulWidget {
  const _ExerciseMediaSheet({
    required this.exercise,
    required this.mediaAssetLoader,
    this.videoMediaBuilder,
    this.restRemainingSeconds,
  });

  final ExecutionExercise exercise;
  final Future<MediaAsset?> Function(String id) mediaAssetLoader;
  final VideoMediaBuilder? videoMediaBuilder;
  final int? restRemainingSeconds;

  @override
  State<_ExerciseMediaSheet> createState() => _ExerciseMediaSheetState();
}

final class _ExerciseMediaSheetState extends State<_ExerciseMediaSheet> {
  late final PageController _pageController;
  late _MediaSheetSection _section;
  var _imageIndex = 0;

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
    _section = widget.exercise.imageMediaIds.isNotEmpty
        ? _MediaSheetSection.images
        : _MediaSheetSection.video;
  }

  @override
  void dispose() {
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final exercise = widget.exercise;
    final hasImages = exercise.imageMediaIds.isNotEmpty;
    final hasVideo = exercise.videoMediaId != null;
    return SizedBox(
      height: MediaQuery.of(context).size.height * 0.95,
      child: Scaffold(
        appBar: AppBar(
          title: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Médias de l’exercice'),
              Text(exercise.name, style: Theme.of(context).textTheme.bodySmall),
            ],
          ),
          automaticallyImplyLeading: false,
          actions: [
            IconButton(
              tooltip: 'Fermer',
              onPressed: () => Navigator.of(context).pop(),
              icon: const Icon(Icons.close),
            ),
          ],
        ),
        body: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            if (widget.restRemainingSeconds != null) ...[
              _RestPlanBanner(remainingSeconds: widget.restRemainingSeconds!),
              const SizedBox(height: 16),
            ],
            if (hasImages && hasVideo) ...[
              Wrap(
                spacing: 8,
                children: [
                  ChoiceChip(
                    label: const Text('Images'),
                    selected: _section == _MediaSheetSection.images,
                    onSelected: (_) {
                      setState(() => _section = _MediaSheetSection.images);
                    },
                  ),
                  ChoiceChip(
                    label: const Text('Vidéo'),
                    selected: _section == _MediaSheetSection.video,
                    onSelected: (_) {
                      setState(() => _section = _MediaSheetSection.video);
                    },
                  ),
                ],
              ),
              const SizedBox(height: 16),
            ],
            if (_section == _MediaSheetSection.images && hasImages)
              _ImageMediaGallery(
                imageMediaIds: exercise.imageMediaIds,
                pageController: _pageController,
                currentIndex: _imageIndex,
                onPageChanged: (index) => setState(() => _imageIndex = index),
                mediaAssetLoader: widget.mediaAssetLoader,
              )
            else if (hasVideo)
              _VideoMediaPanel(
                videoMediaId: exercise.videoMediaId!,
                mediaAssetLoader: widget.mediaAssetLoader,
                videoMediaBuilder: widget.videoMediaBuilder,
              ),
          ],
        ),
      ),
    );
  }
}

final class _ImageMediaGallery extends StatelessWidget {
  const _ImageMediaGallery({
    required this.imageMediaIds,
    required this.pageController,
    required this.currentIndex,
    required this.onPageChanged,
    required this.mediaAssetLoader,
  });

  final List<String> imageMediaIds;
  final PageController pageController;
  final int currentIndex;
  final ValueChanged<int> onPageChanged;
  final Future<MediaAsset?> Function(String id) mediaAssetLoader;

  @override
  Widget build(BuildContext context) {
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Align(
            alignment: Alignment.centerRight,
            child: Text(
              '${currentIndex + 1}/${imageMediaIds.length}',
              style: Theme.of(context).textTheme.labelLarge?.copyWith(
                color: Theme.of(context).colorScheme.primary,
              ),
            ),
          ),
          const SizedBox(height: 12),
          SizedBox(
            height: 320,
            child: PageView.builder(
              controller: pageController,
              itemCount: imageMediaIds.length,
              onPageChanged: onPageChanged,
              itemBuilder: (context, index) {
                return _ImageMediaPage(
                  mediaAssetId: imageMediaIds[index],
                  index: index,
                  mediaAssetLoader: mediaAssetLoader,
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

final class _ImageMediaPage extends StatelessWidget {
  const _ImageMediaPage({
    required this.mediaAssetId,
    required this.index,
    required this.mediaAssetLoader,
  });

  final String mediaAssetId;
  final int index;
  final Future<MediaAsset?> Function(String id) mediaAssetLoader;

  @override
  Widget build(BuildContext context) {
    final tokens = courtBlazerTokensOf(context);
    return Container(
      margin: const EdgeInsets.symmetric(horizontal: 4),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(6),
        border: Border.all(color: tokens.border),
        color: Theme.of(context).colorScheme.surface,
      ),
      child: FutureBuilder<MediaAsset?>(
        future: mediaAssetLoader(mediaAssetId),
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return const Center(child: CircularProgressIndicator());
          }
          final asset = snapshot.data;
          if (asset == null || asset.kind != MediaKind.image) {
            return _MissingMediaPlaceholder(
              icon: Icons.broken_image_outlined,
              title: 'Image indisponible',
              subtitle: mediaAssetId,
            );
          }
          return ClipRRect(
            borderRadius: BorderRadius.circular(6),
            child: Image.file(
              _fileFromLocalUri(asset.localUri),
              key: ValueKey('exercise-image-${asset.metadata.id}'),
              fit: BoxFit.contain,
              errorBuilder: (context, error, stackTrace) {
                return _MissingMediaPlaceholder(
                  icon: Icons.broken_image_outlined,
                  title: 'Image introuvable',
                  subtitle: asset.metadata.id,
                );
              },
            ),
          );
        },
      ),
    );
  }
}

final class _VideoMediaPanel extends StatelessWidget {
  const _VideoMediaPanel({
    required this.videoMediaId,
    required this.mediaAssetLoader,
    this.videoMediaBuilder,
  });

  final String videoMediaId;
  final Future<MediaAsset?> Function(String id) mediaAssetLoader;
  final VideoMediaBuilder? videoMediaBuilder;

  @override
  Widget build(BuildContext context) {
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.all(16),
      child: FutureBuilder<MediaAsset?>(
        future: mediaAssetLoader(videoMediaId),
        builder: (context, snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return const SizedBox(
              height: 260,
              child: Center(child: CircularProgressIndicator()),
            );
          }
          final asset = snapshot.data;
          if (asset == null || asset.kind != MediaKind.video) {
            return SizedBox(
              height: 260,
              child: _MissingMediaPlaceholder(
                icon: Icons.videocam_off_outlined,
                title: 'Vidéo indisponible',
                subtitle: videoMediaId,
              ),
            );
          }
          final builder = videoMediaBuilder;
          if (builder != null) {
            return builder(context, asset);
          }
          return _VideoPlayerSurface(asset: asset);
        },
      ),
    );
  }
}

final class _VideoPlayerSurface extends StatefulWidget {
  const _VideoPlayerSurface({required this.asset});

  final MediaAsset asset;

  @override
  State<_VideoPlayerSurface> createState() => _VideoPlayerSurfaceState();
}

final class _VideoPlayerSurfaceState extends State<_VideoPlayerSurface> {
  late final VideoPlayerController _controller;
  late final Future<void> _initialize;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.file(
      _fileFromLocalUri(widget.asset.localUri),
    );
    _initialize = _controller.initialize().then((_) {
      if (mounted) setState(() {});
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<void>(
      future: _initialize,
      builder: (context, snapshot) {
        if (snapshot.connectionState != ConnectionState.done) {
          return const SizedBox(
            height: 260,
            child: Center(child: CircularProgressIndicator()),
          );
        }
        if (snapshot.hasError || !_controller.value.isInitialized) {
          return SizedBox(
            height: 260,
            child: _MissingMediaPlaceholder(
              icon: Icons.videocam_off_outlined,
              title: 'Vidéo introuvable',
              subtitle: widget.asset.metadata.id,
            ),
          );
        }
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            AspectRatio(
              aspectRatio: _controller.value.aspectRatio,
              child: VideoPlayer(_controller),
            ),
            const SizedBox(height: 12),
            IconButton.filled(
              tooltip: _controller.value.isPlaying ? 'Pause' : 'Lecture',
              onPressed: () async {
                if (_controller.value.isPlaying) {
                  await _controller.pause();
                } else {
                  await _controller.play();
                }
                if (mounted) setState(() {});
              },
              icon: Icon(
                _controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
              ),
            ),
          ],
        );
      },
    );
  }
}

final class _MissingMediaPlaceholder extends StatelessWidget {
  const _MissingMediaPlaceholder({
    required this.icon,
    required this.title,
    required this.subtitle,
  });

  final IconData icon;
  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(icon, size: 64, color: Theme.of(context).colorScheme.primary),
          const SizedBox(height: 16),
          Text(title, style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 4),
          Text(subtitle, style: Theme.of(context).textTheme.bodySmall),
        ],
      ),
    );
  }
}

File _fileFromLocalUri(String localUri) {
  final uri = Uri.tryParse(localUri);
  if (uri != null && uri.hasScheme) {
    return File.fromUri(uri);
  }
  return File(localUri);
}

final class _WorkoutPlanSetTile extends StatefulWidget {
  const _WorkoutPlanSetTile({
    required this.position,
    required this.currentPosition,
    required this.exercise,
    required this.onUpdated,
    required this.activeUseCases,
    required this.sessionId,
    this.stepUseCases,
    this.activeRest,
    this.state,
  });

  final ExecutionPosition position;
  final ExecutionPosition currentPosition;
  final ExecutionExercise exercise;
  final SetResultPositionState? state;
  final Future<void> Function() onUpdated;
  final ActiveWorkoutSessionUseCases activeUseCases;
  final ActiveExerciseStepUseCases? stepUseCases;
  final String sessionId;
  final ActiveRestState? activeRest;

  @override
  State<_WorkoutPlanSetTile> createState() => _WorkoutPlanSetTileState();
}

final class _WorkoutPlanSetTileState extends State<_WorkoutPlanSetTile> {
  late final Future<ActiveExerciseStepProgressView?> _stepProgress;

  @override
  void initState() {
    super.initState();
    _stepProgress = _loadStepProgress();
  }

  Future<ActiveExerciseStepProgressView?> _loadStepProgress() async {
    final stepUseCases = widget.stepUseCases;
    if (stepUseCases == null || widget.exercise.steps.isEmpty) {
      return null;
    }
    final planState = _planSetState(
      widget.position,
      widget.currentPosition,
      widget.state,
    );
    if (planState == _PlanSetStatus.todo) {
      return null;
    }
    return stepUseCases.readProgress(
      sessionId: widget.sessionId,
      programIndex: widget.position.programIndex,
      exerciseIndex: widget.position.exerciseIndex,
      setIndex: widget.position.setIndex,
    );
  }

  @override
  Widget build(BuildContext context) {
    final planState = _planSetState(
      widget.position,
      widget.currentPosition,
      widget.state,
    );
    final tappable = planState != _PlanSetStatus.todo;
    final VoidCallback? onTap = tappable
        ? () => unawaited(_handleTap(context, planState))
        : null;
    final tokens = courtBlazerTokensOf(context);
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(6),
        side: BorderSide(
          color: planState == _PlanSetStatus.current
              ? Theme.of(context).colorScheme.primary
              : tokens.border,
        ),
      ),
      child: ListTile(
        enabled: tappable,
        title: Text('Série ${widget.position.setIndex + 1}'),
        subtitle: _WorkoutPlanSetSubtitle(
          status: planState,
          result: widget.state?.result,
          exercise: widget.exercise,
          stepProgress: _stepProgress,
        ),
        trailing: _PlanStatusBadge(status: planState, onTap: onTap),
        onTap: onTap,
      ),
    );
  }

  Future<void> _handleTap(BuildContext context, _PlanSetStatus status) async {
    if (status == _PlanSetStatus.current) {
      Navigator.of(context).pop();
      return;
    }
    await showModalBottomSheet<void>(
      context: context,
      isScrollControlled: true,
      useSafeArea: true,
      builder: (context) => _EditSetResultSheet(
        sessionId: widget.sessionId,
        position: widget.position,
        exercise: widget.exercise,
        state: widget.state,
        status: status,
        activeUseCases: widget.activeUseCases,
        activeRest: widget.activeRest,
        onSaved: widget.onUpdated,
      ),
    );
  }
}

final class _WorkoutPlanSetSubtitle extends StatelessWidget {
  const _WorkoutPlanSetSubtitle({
    required this.status,
    required this.result,
    required this.exercise,
    required this.stepProgress,
  });

  final _PlanSetStatus status;
  final ActiveSetResult? result;
  final ExecutionExercise exercise;
  final Future<ActiveExerciseStepProgressView?> stepProgress;

  @override
  Widget build(BuildContext context) {
    final resultLabel = _planSetSubtitle(status, result, exercise);
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        if (resultLabel.isNotEmpty) Text(resultLabel),
        if (exercise.steps.isNotEmpty)
          FutureBuilder<ActiveExerciseStepProgressView?>(
            future: stepProgress,
            builder: (context, snapshot) {
              final progress = snapshot.data;
              final label = progress == null
                  ? 'Séquence : 0/${exercise.steps.length} étapes'
                  : _planStepSequenceSummary(progress);
              return Padding(
                padding: EdgeInsets.only(top: resultLabel.isEmpty ? 0 : 4),
                child: Text(label),
              );
            },
          ),
      ],
    );
  }
}

final class _EditSetResultSheet extends StatefulWidget {
  const _EditSetResultSheet({
    required this.sessionId,
    required this.position,
    required this.exercise,
    required this.state,
    required this.status,
    required this.activeUseCases,
    required this.onSaved,
    this.activeRest,
  });

  final String sessionId;
  final ExecutionPosition position;
  final ExecutionExercise exercise;
  final SetResultPositionState? state;
  final _PlanSetStatus status;
  final ActiveWorkoutSessionUseCases activeUseCases;
  final Future<void> Function() onSaved;
  final ActiveRestState? activeRest;

  @override
  State<_EditSetResultSheet> createState() => _EditSetResultSheetState();
}

final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
  final _timeController = TextEditingController();
  final _repsController = TextEditingController();
  final _scoreController = TextEditingController();
  Timer? _timer;
  late int _remainingRestSeconds;

  @override
  void initState() {
    super.initState();
    final result = widget.state?.result;
    final actualTimeMs = result?.actualTimeMs;
    if (actualTimeMs != null) {
      _timeController.text = (actualTimeMs / 1000).round().toString();
    }
    final actualReps = result?.actualReps;
    if (actualReps != null) {
      _repsController.text = actualReps.toString();
    }
    final actualScore = result?.actualScore;
    if (actualScore != null) {
      _scoreController.text = _formatScore(actualScore);
    }
    final actualScoreTimeMs = result?.actualScoreTimeMs;
    if (actualScoreTimeMs != null) {
      _scoreController.text = (actualScoreTimeMs / 1000).toStringAsFixed(1);
    }
    _remainingRestSeconds = _remainingSecondsForRest(widget.activeRest);
    if (widget.activeRest != null && _remainingRestSeconds > 0) {
      _timer = Timer.periodic(const Duration(seconds: 1), (_) {
        if (!mounted) return;
        setState(() {
          _remainingRestSeconds = (_remainingRestSeconds - 1)
              .clamp(0, 9999)
              .toInt();
        });
      });
    }
  }

  @override
  void dispose() {
    _timer?.cancel();
    _timeController.dispose();
    _repsController.dispose();
    _scoreController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final isCompleted = widget.status == _PlanSetStatus.completed;
    return Padding(
      padding: EdgeInsets.only(
        left: 16,
        right: 16,
        top: 16,
        bottom: MediaQuery.of(context).viewInsets.bottom + 16,
      ),
      child: ListView(
        shrinkWrap: true,
        children: [
          Row(
            children: [
              Expanded(
                child: Text(
                  'Modifier la série',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
              ),
              IconButton(
                tooltip: 'Annuler',
                onPressed: () => Navigator.of(context).pop(),
                icon: const Icon(Icons.close),
              ),
            ],
          ),
          const SizedBox(height: 8),
          Text(
            '${widget.exercise.name} · '
            'Série ${widget.position.setIndex + 1}/${widget.exercise.setsCount}',
          ),
          const SizedBox(height: 8),
          Text(
            'État actuel : ${isCompleted ? 'Terminée' : 'Passée'}',
            style: Theme.of(context).textTheme.bodySmall,
          ),
          if (widget.activeRest != null) ...[
            const SizedBox(height: 12),
            _RestPlanBanner(remainingSeconds: _remainingRestSeconds),
          ],
          const SizedBox(height: 24),
          if (widget.exercise.timeEnabled) ...[
            TextField(
              controller: _timeController,
              decoration: const InputDecoration(labelText: 'Temps réalisé (s)'),
              keyboardType: TextInputType.number,
            ),
            const SizedBox(height: 12),
          ],
          if (widget.exercise.repsEnabled) ...[
            TextField(
              controller: _repsController,
              decoration: const InputDecoration(labelText: 'Répétitions'),
              keyboardType: TextInputType.number,
            ),
            const SizedBox(height: 12),
          ],
          if (widget.exercise.scoreEnabled) ...[
            TextField(
              controller: _scoreController,
              decoration: InputDecoration(
                labelText: widget.exercise.stopwatchScoreEnabled
                    ? 'Temps réalisé (chrono score, s)'
                    : widget.exercise.scoreUnit == null
                    ? 'Score'
                    : 'Score (${widget.exercise.scoreUnit})',
              ),
              keyboardType: TextInputType.number,
            ),
            const SizedBox(height: 12),
          ],
          const SizedBox(height: 12),
          FilledButton(
            onPressed: _saveCompleted,
            child: Text(
              isCompleted
                  ? 'Enregistrer les corrections'
                  : 'Enregistrer le résultat',
            ),
          ),
          const SizedBox(height: 8),
          OutlinedButton(
            onPressed: _markSkipped,
            child: const Text('Marquer comme passée'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('Annuler'),
          ),
        ],
      ),
    );
  }

  Future<void> _saveCompleted() async {
    if (!_hasAnyValue()) {
      if (widget.status == _PlanSetStatus.completed) {
        final confirmed = await _confirmMarkSkipped();
        if (confirmed != true) return;
      }
      await _saveSkipped();
      return;
    }
    await widget.activeUseCases.upsertSetResultAtPosition(
      sessionId: widget.sessionId,
      programIndex: widget.position.programIndex,
      exerciseIndex: widget.position.exerciseIndex,
      setIndex: widget.position.setIndex,
      status: SetResultStatus.completed,
      actualTimeMs: widget.exercise.timeEnabled
          ? _secondsToMilliseconds(_timeController.text)
          : null,
      actualReps: widget.exercise.repsEnabled
          ? int.tryParse(_repsController.text.trim())
          : null,
      actualScore: widget.exercise.manualScoreEnabled
          ? double.tryParse(_scoreController.text.trim())
          : null,
      actualScoreTimeMs: widget.exercise.stopwatchScoreEnabled
          ? _decimalSecondsToMilliseconds(_scoreController.text)
          : null,
      scoreInputModeSnapshot: widget.exercise.scoreInputMode,
      scoreLabelSnapshot: widget.exercise.scoreLabel,
      scoreUnitSnapshot: widget.exercise.scoreUnit,
    );
    await _finishSave();
  }

  Future<void> _markSkipped() async {
    await _saveSkipped();
  }

  Future<void> _saveSkipped() async {
    await widget.activeUseCases.upsertSetResultAtPosition(
      sessionId: widget.sessionId,
      programIndex: widget.position.programIndex,
      exerciseIndex: widget.position.exerciseIndex,
      setIndex: widget.position.setIndex,
      status: SetResultStatus.skipped,
    );
    await _finishSave();
  }

  Future<void> _finishSave() async {
    await widget.onSaved();
    if (!mounted) return;
    Navigator.of(context).pop();
  }

  bool _hasAnyValue() {
    return _timeController.text.trim().isNotEmpty ||
        _repsController.text.trim().isNotEmpty ||
        _scoreController.text.trim().isNotEmpty;
  }

  Future<bool?> _confirmMarkSkipped() {
    return showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Supprimer le résultat de cette série ?'),
        content: const Text(
          'La série restera dans le plan comme passée, sans résultat saisi.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(false),
            child: const Text('Annuler'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(context).pop(true),
            child: const Text('Marquer comme passée'),
          ),
        ],
      ),
    );
  }
}

final class _PlanStatusBadge extends StatelessWidget {
  const _PlanStatusBadge({required this.status, this.onTap});

  final _PlanSetStatus status;
  final VoidCallback? onTap;

  @override
  Widget build(BuildContext context) {
    final tokens = courtBlazerTokensOf(context);
    final label = switch (status) {
      _PlanSetStatus.todo => 'À faire',
      _PlanSetStatus.current => 'En cours',
      _PlanSetStatus.completed => 'Terminée',
      _PlanSetStatus.passed => 'Passée',
    };
    final color = switch (status) {
      _PlanSetStatus.completed => tokens.success,
      _PlanSetStatus.current => Theme.of(context).colorScheme.primary,
      _ => tokens.border,
    };
    return InkWell(
      onTap: onTap,
      borderRadius: BorderRadius.circular(16),
      child: Chip(
        label: Text(label),
        backgroundColor: color.withAlpha(35),
        side: BorderSide(color: color),
      ),
    );
  }
}

enum _PlanSetStatus { todo, current, completed, passed }

_PlanSetStatus _planSetState(
  ExecutionPosition position,
  ExecutionPosition currentPosition,
  SetResultPositionState? state,
) {
  if (_comparePosition(position, currentPosition) == 0) {
    return _PlanSetStatus.current;
  }
  if (state?.status == SetPositionStatus.completed) {
    return _PlanSetStatus.completed;
  }
  if (state?.status == SetPositionStatus.skipped ||
      _comparePosition(position, currentPosition) < 0) {
    return _PlanSetStatus.passed;
  }
  return _PlanSetStatus.todo;
}

String _planSetSubtitle(
  _PlanSetStatus status,
  ActiveSetResult? result,
  ExecutionExercise exercise,
) {
  if (status == _PlanSetStatus.completed && result != null) {
    return _setResultSummary(result, exercise: exercise);
  }
  if (status == _PlanSetStatus.passed) {
    return 'Aucun résultat';
  }
  return '';
}

String _planStepSequenceSummary(ActiveExerciseStepProgressView view) {
  final state = view.state;
  final stepsCount = view.steps.length;
  final passageIndex =
      state.status == ActiveExerciseStepProgressStatus.sequenceComplete
      ? (view.expectedPassages - 1).clamp(0, 9999).toInt()
      : state.currentPassageIndex;
  final completedInPassage =
      state.status == ActiveExerciseStepProgressStatus.sequenceComplete
      ? stepsCount
      : view.results
            .where((result) => result.passageIndex == passageIndex)
            .map((result) => result.stepIndex)
            .toSet()
            .length
            .clamp(0, stepsCount)
            .toInt();
  final passageLabel = view.expectedPassages > 1
      ? ' · Passage ${passageIndex + 1}/${view.expectedPassages}'
      : '';
  return 'Séquence : $completedInPassage/$stepsCount étapes$passageLabel';
}

String _setResultSummary(
  ActiveSetResult result, {
  ExecutionExercise? exercise,
}) {
  final parts = <String>[];
  if (result.actualTimeMs != null) {
    parts.add(_formatDuration(Duration(milliseconds: result.actualTimeMs!)));
  }
  if (result.actualReps != null) {
    parts.add('${result.actualReps} reps');
  }
  if (result.actualScore != null) {
    final value = _formatScore(result.actualScore!);
    final unit = result.scoreUnitSnapshot?.trim();
    parts.add(unit == null || unit.isEmpty ? 'Score $value' : '$value $unit');
  }
  if (result.actualScoreTimeMs != null) {
    final value = _formatScoreStopwatch(
      Duration(milliseconds: result.actualScoreTimeMs!),
    );
    final targetScoreTimeMs = exercise?.targetScoreTimeMs;
    if (targetScoreTimeMs == null) {
      parts.add('Score chrono $value');
    } else {
      final targetValue = _formatScoreStopwatchTarget(
        Duration(milliseconds: targetScoreTimeMs),
      );
      parts.add('Score chrono $value / objectif $targetValue');
    }
  }
  return parts.isEmpty ? 'Aucun résultat' : parts.join(' · ');
}

String _formatScore(double value) {
  return value == value.roundToDouble()
      ? value.round().toString()
      : value.toString();
}

int? _secondsToMilliseconds(String rawValue) {
  final seconds = int.tryParse(rawValue.trim());
  return seconds == null ? null : seconds * 1000;
}

int? _decimalSecondsToMilliseconds(String rawValue) {
  final seconds = double.tryParse(rawValue.trim());
  return seconds == null ? null : (seconds * 1000).round();
}

int _remainingSecondsForRest(ActiveRestState? rest) {
  if (rest == null) return 0;
  final elapsedSeconds = DateTime.now()
      .toUtc()
      .difference(rest.startedAt.toUtc())
      .inSeconds;
  return (rest.adjustedRestSeconds - elapsedSeconds).clamp(0, 9999).toInt();
}

String _stateKey(SetResultPositionState state) {
  return _positionKey(state.programIndex, state.exerciseIndex, state.setIndex);
}

String _positionKey(int programIndex, int exerciseIndex, int setIndex) {
  return '$programIndex:$exerciseIndex:$setIndex';
}

int _comparePosition(ExecutionPosition left, ExecutionPosition right) {
  final program = left.programIndex.compareTo(right.programIndex);
  if (program != 0) return program;
  final exercise = left.exerciseIndex.compareTo(right.exerciseIndex);
  if (exercise != 0) return exercise;
  return left.setIndex.compareTo(right.setIndex);
}

final class WorkoutExecutionPlan {
  const WorkoutExecutionPlan({required this.name, required this.programs});

  factory WorkoutExecutionPlan.fromSession(ActiveWorkoutSession session) {
    final snapshot =
        jsonDecode(session.resolvedTemplateSnapshotJson)
            as Map<String, dynamic>;
    final overrides = snapshot['overrides'] as List<dynamic>? ?? const [];
    final programs = (snapshot['programs'] as List<dynamic>? ?? const []).map((
      rawProgram,
    ) {
      final program = rawProgram as Map<String, dynamic>;
      final programSnapshot =
          jsonDecode(program['programSnapshotJson'] as String)
              as Map<String, dynamic>;
      return ExecutionProgram.fromSnapshot(
        id: program['id'] as String,
        name: program['programNameSnapshot'] as String,
        snapshot: programSnapshot,
        overrides: overrides,
      );
    }).toList();
    return WorkoutExecutionPlan(
      name: snapshot['name'] as String? ?? 'Séance',
      programs: programs,
    );
  }

  final String name;
  final List<ExecutionProgram> programs;

  ExecutionProgram programAt(ExecutionPosition position) {
    return programs[position.programIndex];
  }

  ExecutionExercise exerciseAt(ExecutionPosition position) {
    return programAt(position).exercises[position.exerciseIndex];
  }

  ExecutionPosition? nextPosition(ExecutionPosition position) {
    final exercise = exerciseAt(position);
    if (position.setIndex + 1 < exercise.setsCount) {
      return position.copyWith(setIndex: position.setIndex + 1);
    }
    final program = programAt(position);
    if (position.exerciseIndex + 1 < program.exercises.length) {
      return ExecutionPosition(
        programIndex: position.programIndex,
        exerciseIndex: position.exerciseIndex + 1,
        setIndex: 0,
      );
    }
    if (position.programIndex + 1 < programs.length) {
      return ExecutionPosition(
        programIndex: position.programIndex + 1,
        exerciseIndex: 0,
        setIndex: 0,
      );
    }
    return null;
  }

  bool shouldShowRestAfter(ExecutionPosition position) {
    return nextPosition(position) != null &&
        exerciseAt(position).restSeconds > 0;
  }

  String progressLabel(ExecutionPosition position) {
    final program = programAt(position);
    return 'Programme ${position.programIndex + 1}/${programs.length} · '
        'Exercice ${position.exerciseIndex + 1}/${program.exercises.length}';
  }
}

final class ExecutionProgram {
  const ExecutionProgram({
    required this.id,
    required this.name,
    required this.exercises,
  });

  factory ExecutionProgram.fromSnapshot({
    required String id,
    required String name,
    required Map<String, dynamic> snapshot,
    required List<dynamic> overrides,
  }) {
    final exercises = (snapshot['exercises'] as List<dynamic>? ?? const []).map(
      (raw) {
        final exercise = raw as Map<String, dynamic>;
        Map<String, dynamic>? override;
        for (final rawOverride in overrides) {
          final item = rawOverride as Map<String, dynamic>;
          if (item['workoutTemplateProgramId'] == id &&
              item['snapshotProgramExerciseId'] == exercise['id']) {
            override = item;
            break;
          }
        }
        return ExecutionExercise.fromSnapshot(exercise, override);
      },
    ).toList();
    return ExecutionProgram(id: id, name: name, exercises: exercises);
  }

  final String id;
  final String name;
  final List<ExecutionExercise> exercises;
}

final class ExecutionExercise {
  const ExecutionExercise({
    required this.id,
    required this.name,
    required this.setsCount,
    required this.timeEnabled,
    required this.repsEnabled,
    required this.scoreEnabled,
    required this.restSeconds,
    this.imageMediaIds = const [],
    this.steps = const [],
    this.videoMediaId,
    this.scoreInputMode = ScoreInputMode.manual,
    this.scoreLabel,
    this.targetTimeSeconds,
    this.targetReps,
    this.targetScore,
    this.targetScoreTimeMs,
    this.scoreUnit,
  });

  factory ExecutionExercise.fromSnapshot(
    Map<String, dynamic> exercise,
    Map<String, dynamic>? override,
  ) {
    return ExecutionExercise(
      id: exercise['id'] as String,
      name: exercise['exerciseNameSnapshot'] as String,
      setsCount:
          (override?['setsCountOverride'] as int?) ??
          (exercise['setsCount'] as int),
      timeEnabled: exercise['timeEnabled'] as bool,
      repsEnabled: exercise['repsEnabled'] as bool,
      scoreEnabled: exercise['scoreEnabled'] as bool,
      restSeconds: exercise['restSecondsOverride'] as int? ?? 0,
      imageMediaIds: _imageMediaIdsFromSnapshot(exercise),
      steps: _exerciseStepsFromSnapshot(exercise),
      videoMediaId:
          exercise['videoMediaIdSnapshot'] as String? ??
          exercise['exerciseVideoMediaIdSnapshot'] as String?,
      scoreInputMode: _scoreInputModeFromSnapshot(
        exercise['scoreInputModeSnapshot'],
      ),
      scoreLabel: exercise['scoreLabelSnapshot'] as String?,
      targetTimeSeconds:
          (override?['targetTimeSecondsOverride'] as int?) ??
          (exercise['targetTimeSeconds'] as int?),
      targetReps:
          (override?['targetRepsOverride'] as int?) ??
          (exercise['targetReps'] as int?),
      targetScore:
          (override?['targetScoreOverride'] as num?)?.toDouble() ??
          (exercise['targetScore'] as num?)?.toDouble(),
      targetScoreTimeMs:
          (override?['targetScoreTimeMsOverride'] as int?) ??
          (exercise['targetScoreTimeMs'] as int?),
      scoreUnit: exercise['scoreUnitSnapshot'] as String?,
    );
  }

  final String id;
  final String name;
  final int setsCount;
  final bool timeEnabled;
  final bool repsEnabled;
  final bool scoreEnabled;
  final int restSeconds;
  final List<String> imageMediaIds;
  final List<ExerciseStep> steps;
  final String? videoMediaId;
  final ScoreInputMode scoreInputMode;
  final String? scoreLabel;
  final int? targetTimeSeconds;
  final int? targetReps;
  final double? targetScore;
  final int? targetScoreTimeMs;
  final String? scoreUnit;

  bool get manualScoreEnabled {
    return scoreEnabled && scoreInputMode == ScoreInputMode.manual;
  }

  bool get stopwatchScoreEnabled {
    return scoreEnabled && scoreInputMode == ScoreInputMode.stopwatch;
  }

  bool get hasMedia {
    return imageMediaIds.isNotEmpty || videoMediaId != null;
  }
}

List<String> _imageMediaIdsFromSnapshot(Map<String, dynamic> exercise) {
  final imageMediaIds =
      exercise['imageMediaIdsSnapshot'] ??
      exercise['exerciseImageMediaIdsSnapshot'];
  if (imageMediaIds is List) {
    return imageMediaIds
        .whereType<String>()
        .where((id) => id.trim().isNotEmpty)
        .toList(growable: false);
  }
  final imageMediaId = exercise['exerciseImageMediaIdSnapshot'] as String?;
  return [
    if (imageMediaId != null && imageMediaId.trim().isNotEmpty) imageMediaId,
  ];
}

List<ExerciseStep> _exerciseStepsFromSnapshot(Map<String, dynamic> exercise) {
  final rawSteps = exercise['exerciseStepsSnapshot'];
  if (rawSteps is! List) return const [];
  return rawSteps
      .map((rawStep) {
        final step = rawStep as Map<String, dynamic>;
        final typeName = step['type'] as String? ?? ExerciseStepType.time.name;
        return ExerciseStep(
          id: step['id'] as String,
          position: step['position'] as int,
          name: step['name'] as String,
          type: ExerciseStepType.values.firstWhere(
            (type) => type.name == typeName,
            orElse: () => ExerciseStepType.time,
          ),
          defaultTargetValue: step['defaultTargetValue'] as int,
          hasScore: step['hasScore'] as bool? ?? false,
          scoreInputMode: _scoreInputModeFromSnapshot(step['scoreInputMode']),
          scoreLabel: step['scoreLabel'] as String?,
          scoreUnit: step['scoreUnit'] as String?,
          defaultTargetScore: (step['defaultTargetScore'] as num?)?.toDouble(),
          defaultTargetScoreTimeMs: step['defaultTargetScoreTimeMs'] as int?,
        );
      })
      .toList(growable: false);
}

ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {
  return switch (value) {
    'stopwatch' => ScoreInputMode.stopwatch,
    _ => ScoreInputMode.manual,
  };
}

final class ExecutionPosition {
  const ExecutionPosition({
    required this.programIndex,
    required this.exerciseIndex,
    required this.setIndex,
  });

  final int programIndex;
  final int exerciseIndex;
  final int setIndex;

  ExecutionPosition copyWith({int? setIndex}) {
    return ExecutionPosition(
      programIndex: programIndex,
      exerciseIndex: exerciseIndex,
      setIndex: setIndex ?? this.setIndex,
    );
  }
}

final class _Header extends StatelessWidget {
  const _Header({required this.elapsedLabel, required this.progressLabel});

  final String elapsedLabel;
  final String progressLabel;

  @override
  Widget build(BuildContext context) {
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.all(14),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(elapsedLabel, style: AppTextStyles.scoreNumber(context)),
          const SizedBox(height: 4),
          Text(progressLabel),
        ],
      ),
    );
  }
}

final class _SeriesCounterCard extends StatelessWidget {
  const _SeriesCounterCard({required this.currentSet, required this.totalSets});

  final int currentSet;
  final int totalSets;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final tokens = courtBlazerTokensOf(context);
    return CourtBlazerAccentPanel(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'SÉRIE',
            style: theme.textTheme.labelSmall?.copyWith(
              color: tokens.mutedText,
              fontWeight: FontWeight.w600,
            ),
          ),
          const SizedBox(height: 6),
          Text(
            '$currentSet / $totalSets',
            style: theme.textTheme.displayLarge?.copyWith(
              color: theme.colorScheme.primary,
              fontSize: 48,
            ),
          ),
        ],
      ),
    );
  }
}

String _formatDuration(Duration duration) {
  final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
  final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
  final hours = duration.inHours;
  if (hours > 0) {
    return '$hours:$minutes:$seconds';
  }
  return '$minutes:$seconds';
}

String _formatStepCountdown(Duration duration) {
  final totalSeconds = (duration.inMilliseconds / 1000)
      .ceil()
      .clamp(0, 9999)
      .toInt();
  final minutes = (totalSeconds ~/ 60).toString().padLeft(2, '0');
  final seconds = (totalSeconds % 60).toString().padLeft(2, '0');
  return '$minutes:$seconds';
}

String _formatScoreStopwatch(Duration duration) {
  final totalTenths = (duration.inMilliseconds / 100).floor();
  final minutes = (totalTenths ~/ 600).toString().padLeft(2, '0');
  final seconds = ((totalTenths ~/ 10) % 60).toString().padLeft(2, '0');
  final tenths = totalTenths % 10;
  return '$minutes:$seconds.$tenths';
}

String _formatScoreStopwatchTarget(Duration duration) {
  if (duration.inMilliseconds % 1000 == 0) {
    return _formatDuration(duration);
  }
  return _formatScoreStopwatch(duration);
}
