import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:watch_bridge_contract/watch_bridge_contract.dart';

import '../application/watch_session_view_model.dart';

typedef WatchNowEpochMs = int Function();

int _defaultNowEpochMs() {
  return DateTime.now().toUtc().millisecondsSinceEpoch;
}

final class WatchSessionScreen extends StatefulWidget {
  const WatchSessionScreen({
    required this.viewModel,
    this.nowEpochMs = _defaultNowEpochMs,
    super.key,
  });

  final WatchSessionViewModel viewModel;
  final WatchNowEpochMs nowEpochMs;

  @override
  State<WatchSessionScreen> createState() => _WatchSessionScreenState();
}

final class _WatchSessionScreenState extends State<WatchSessionScreen> {
  late final PageController _pageController;
  Timer? _ticker;
  Timer? _failureNoticeTimer;
  String? _failureNoticeMessage;
  var _lastFailureNoticeSerial = 0;
  var _returnToSessionAfterSecondarySuccess = false;
  var _secondaryActionStartFailureSerial = 0;
  final _timerRemainingMsByKey = <String, int>{};
  final _completionHapticTimerKeys = <String>{};

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
  }

  @override
  void dispose() {
    _ticker?.cancel();
    _failureNoticeTimer?.cancel();
    _pageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<WatchSessionUiState>(
      valueListenable: widget.viewModel,
      builder: (context, state, _) {
        _syncFailureNotice(state);
        _syncSecondaryNavigation(state);
        final projection = state.projection;
        final nowEpochMs = widget.nowEpochMs();
        _syncTimerCompletionHaptic(projection, nowEpochMs: nowEpochMs);
        _syncUiTicker(projection);
        if (projection.phase == WatchSessionPhase.noActiveSession) {
          final phoneReachable =
              projection.phoneReachable && !state.connectionLost;
          return _RoundScaffold(
            notice: _failureNoticeMessage,
            child: _NoSessionView(
              state: state,
              phoneReachable: phoneReachable,
              onPrimaryAction: widget.viewModel.sendPrimaryAction,
            ),
          );
        }
        final pages = <Widget>[
          _RoundScaffold(
            key: const ValueKey('watch-session-page'),
            notice: _failureNoticeMessage,
            child: _SessionMainView(
              state: state,
              onActions: _showActions,
              onStats: state.hasLiveSensors ? _showStats : null,
              onTogglePause: widget.viewModel.sendPrimaryAction,
              onIncrementScore: widget.viewModel.incrementScore,
              onDecrementScore: widget.viewModel.decrementScore,
              onCompleteStep: widget.viewModel.completeCurrentStep,
              nowEpochMs: nowEpochMs,
            ),
          ),
          _RoundScaffold(
            key: const ValueKey('watch-actions-page'),
            notice: _failureNoticeMessage,
            child: _ActionsView(
              state: state,
              onAction: _handleSecondaryAction,
              onSession: _showSession,
            ),
          ),
        ];
        if (state.hasLiveSensors) {
          pages.add(
            _RoundScaffold(
              key: const ValueKey('watch-stats-page'),
              notice: _failureNoticeMessage,
              child: _StatsView(state: state, onSession: _showSession),
            ),
          );
        }
        return PageView(
          controller: _pageController,
          physics: const _WatchPageScrollPhysics(),
          children: pages,
        );
      },
    );
  }

  void _syncFailureNotice(WatchSessionUiState state) {
    if (state.commandFailureSerial == _lastFailureNoticeSerial ||
        state.commandFailureMessage == null) {
      return;
    }
    _lastFailureNoticeSerial = state.commandFailureSerial;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted) {
        return;
      }
      _failureNoticeTimer?.cancel();
      setState(() {
        _failureNoticeMessage = state.commandFailureMessage;
      });
      _failureNoticeTimer = Timer(const Duration(milliseconds: 1800), () {
        if (mounted) {
          setState(() {
            _failureNoticeMessage = null;
          });
        }
      });
    });
  }

  void _showActions() {
    _pageController.animateToPage(
      1,
      duration: const Duration(milliseconds: 180),
      curve: Curves.easeOut,
    );
  }

  void _showStats() {
    _pageController.animateToPage(
      2,
      duration: const Duration(milliseconds: 180),
      curve: Curves.easeOut,
    );
  }

  void _showSession() {
    _pageController.animateToPage(
      0,
      duration: const Duration(milliseconds: 180),
      curve: Curves.easeOut,
    );
  }

  Future<void> _handleSecondaryAction(WatchSecondaryAction action) async {
    final confirmed = switch (action) {
      WatchSecondaryAction.skipCurrentPassage => await _confirm(
        title: 'Passer le passage ?',
        message: "L'étape en cours sera ignorée.",
        confirmLabel: 'Passer',
      ),
      WatchSecondaryAction.skipCurrentSet => await _confirm(
        title: 'Passer la série ?',
        message: 'Le chrono en cours sera ignoré.',
        confirmLabel: 'Passer',
      ),
      _ => true,
    };
    if (!confirmed || !mounted) {
      return;
    }
    if (action != WatchSecondaryAction.skipCurrentRest) {
      unawaited(widget.viewModel.sendSecondaryAction(action));
      _showSession();
      return;
    }
    if (mounted) {
      _returnToSessionAfterSecondarySuccess = true;
      _secondaryActionStartFailureSerial =
          widget.viewModel.value.commandFailureSerial;
      await widget.viewModel.sendSecondaryAction(action);
      if (mounted) {
        _syncSecondaryNavigation(widget.viewModel.value);
      }
    }
  }

  void _syncSecondaryNavigation(WatchSessionUiState state) {
    if (!_returnToSessionAfterSecondarySuccess || state.commandPending) {
      return;
    }
    final failed =
        state.connectionLost ||
        state.commandFailureSerial != _secondaryActionStartFailureSerial;
    _returnToSessionAfterSecondarySuccess = false;
    if (failed) {
      return;
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        _showSession();
      }
    });
  }

  void _syncTimerCompletionHaptic(
    WatchSessionProjection projection, {
    required int nowEpochMs,
  }) {
    final timer = _primaryDisplayTimer(projection);
    if (timer == null ||
        timer.displayMode != WatchTimerDisplayMode.countdown ||
        timer.runState != WatchTimerRunState.running) {
      return;
    }
    final key = _timerHapticKey(projection, timer);
    final remainingMs = _displayDuration(
      timer,
      nowEpochMs: nowEpochMs,
    ).inMilliseconds;
    final previousRemainingMs = _timerRemainingMsByKey[key];
    _timerRemainingMsByKey[key] = remainingMs;
    if (remainingMs > 0 ||
        previousRemainingMs == null ||
        previousRemainingMs <= 0 ||
        !_completionHapticTimerKeys.add(key)) {
      return;
    }
    _triggerTimerCompletionHaptic();
  }

  void _syncUiTicker(WatchSessionProjection projection) {
    final shouldTick = _hasRunningVisibleTimer(projection);
    if (shouldTick) {
      _ticker ??= Timer.periodic(const Duration(seconds: 1), (_) {
        if (mounted) {
          setState(() {});
        }
      });
      return;
    }
    _ticker?.cancel();
    _ticker = null;
  }

  void _triggerTimerCompletionHaptic() {
    unawaited(HapticFeedback.heavyImpact());
    unawaited(
      Future<void>.delayed(const Duration(milliseconds: 140), () {
        return HapticFeedback.heavyImpact();
      }),
    );
    unawaited(
      Future<void>.delayed(const Duration(milliseconds: 320), () {
        return HapticFeedback.heavyImpact();
      }),
    );
  }

  Future<bool> _confirm({
    required String title,
    required String message,
    required String confirmLabel,
  }) async {
    final result = await showDialog<bool>(
      context: context,
      builder: (context) {
        return _ConfirmSheet(
          title: title,
          message: message,
          confirmLabel: confirmLabel,
        );
      },
    );
    return result ?? false;
  }
}

final class _WatchPageScrollPhysics extends PageScrollPhysics {
  const _WatchPageScrollPhysics({super.parent});

  @override
  _WatchPageScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return _WatchPageScrollPhysics(parent: buildParent(ancestor));
  }

  @override
  Simulation? createBallisticSimulation(
    ScrollMetrics position,
    double velocity,
  ) {
    if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
        (velocity >= 0.0 && position.pixels >= position.maxScrollExtent)) {
      return super.createBallisticSimulation(position, velocity);
    }
    final viewport = position is PageMetrics
        ? position.viewportDimension * position.viewportFraction
        : position.viewportDimension;
    if (viewport <= 0) {
      return null;
    }
    final target = (position.pixels / viewport).roundToDouble() * viewport;
    final clampedTarget = target.clamp(
      position.minScrollExtent,
      position.maxScrollExtent,
    );
    if (clampedTarget == position.pixels) {
      return null;
    }
    return ScrollSpringSimulation(
      spring,
      position.pixels,
      clampedTarget,
      velocity,
      tolerance: toleranceFor(position),
    );
  }
}

final class _RoundScaffold extends StatelessWidget {
  const _RoundScaffold({required this.child, this.notice, super.key});

  final Widget child;
  final String? notice;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) {
          final diameter = constraints.biggest.shortestSide;
          final contentSize = diameter < 210 ? diameter : 210.0;
          return Center(
            child: SizedBox.square(
              dimension: contentSize,
              child: Padding(
                padding: EdgeInsets.all(contentSize * 0.09),
                child: Stack(
                  children: [
                    Positioned.fill(child: child),
                    if (notice case final message?)
                      Positioned(
                        left: 8,
                        right: 8,
                        bottom: 0,
                        child: _FailureNotice(message),
                      ),
                  ],
                ),
              ),
            ),
          );
        },
      ),
    );
  }
}

final class _FailureNotice extends StatelessWidget {
  const _FailureNotice(this.message);

  final String message;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: const Color(0xFF141824),
        border: Border.all(color: const Color(0xFFD72638)),
        borderRadius: BorderRadius.circular(6),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
        child: Text(
          message,
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 10),
        ),
      ),
    );
  }
}

final class _GtMark extends StatelessWidget {
  const _GtMark();

  @override
  Widget build(BuildContext context) {
    const size = 52.0;
    return Container(
      width: size,
      height: size,
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: const Color(0xFF141824),
        border: Border.all(color: const Color(0xFFC9A24A), width: 2),
        borderRadius: BorderRadius.circular(6),
      ),
      child: Stack(
        alignment: Alignment.center,
        children: [
          Transform.rotate(
            angle: -0.48,
            child: Container(
              width: size * 0.82,
              height: size * 0.18,
              color: const Color(0xFFD72638),
            ),
          ),
          Text(
            'GT',
            style: Theme.of(context).textTheme.displayLarge?.copyWith(
              fontSize: size * 0.46,
              color: const Color(0xFFC9A24A),
              height: 1,
            ),
          ),
        ],
      ),
    );
  }
}

final class _NoSessionView extends StatelessWidget {
  const _NoSessionView({
    required this.state,
    required this.phoneReachable,
    required this.onPrimaryAction,
  });

  final WatchSessionUiState state;
  final bool phoneReachable;
  final VoidCallback onPrimaryAction;

  @override
  Widget build(BuildContext context) {
    final canSend = phoneReachable && state.actionsEnabled;
    final primaryAction = state.projection.primaryAction;
    final showsAction =
        primaryAction != WatchPrimaryAction.none || state.commandPending;
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const _GtMark(),
        const SizedBox(height: 14),
        Text(
          phoneReachable ? 'Aucune séance en cours' : 'Téléphone indisponible',
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.bodySmall,
        ),
        if (showsAction) ...[
          const SizedBox(height: 12),
          SizedBox(
            width: double.infinity,
            child: FilledButton(
              onPressed: canSend ? onPrimaryAction : null,
              child: _ActionButtonLabel(_primaryLabel(state)),
            ),
          ),
        ],
      ],
    );
  }
}

final class _SessionMainView extends StatelessWidget {
  const _SessionMainView({
    required this.state,
    required this.onActions,
    required this.onStats,
    required this.onTogglePause,
    required this.onIncrementScore,
    required this.onDecrementScore,
    required this.onCompleteStep,
    required this.nowEpochMs,
  });

  final WatchSessionUiState state;
  final VoidCallback onActions;
  final VoidCallback? onStats;
  final VoidCallback onTogglePause;
  final VoidCallback onIncrementScore;
  final VoidCallback onDecrementScore;
  final VoidCallback onCompleteStep;
  final int nowEpochMs;

  @override
  Widget build(BuildContext context) {
    final projection = state.projection;
    final isRest =
        projection.phase == WatchSessionPhase.restRunning ||
        projection.phase == WatchSessionPhase.restPaused;
    final connectionLost = state.connectionLost || !projection.phoneReachable;
    final hasActions = projection.secondaryActions.isNotEmpty;
    final timer = _primaryDisplayTimer(projection);
    final showsTimer = timer != null;
    final footerText = _sessionFooterText(state);
    final canToggleTimer =
        showsTimer &&
        !connectionLost &&
        state.actionsEnabled &&
        _timerButtonCommandMatches(
          timer: timer,
          primaryAction: projection.primaryAction,
        );
    return Stack(
      children: [
        Positioned(
          top: 0,
          left: 58,
          right: 58,
          child: Container(
            height: 2,
            decoration: BoxDecoration(
              color: const Color(0xFFD72638),
              borderRadius: BorderRadius.circular(2),
            ),
          ),
        ),
        Positioned.fill(
          top: 18,
          bottom: 18,
          child: Opacity(
            opacity: connectionLost ? 0.48 : 1,
            child: isRest
                ? _RestContent(
                    state: state,
                    onTogglePause: canToggleTimer ? onTogglePause : null,
                    nowEpochMs: nowEpochMs,
                  )
                : _ActiveContent(
                    state: state,
                    onTogglePause: canToggleTimer ? onTogglePause : null,
                    onIncrementScore: onIncrementScore,
                    onDecrementScore: onDecrementScore,
                    onCompleteStep: onCompleteStep,
                    nowEpochMs: nowEpochMs,
                  ),
          ),
        ),
        if (hasActions)
          Positioned(
            top: -7,
            right: -8,
            child: IconButton(
              onPressed: onActions,
              tooltip: 'Actions',
              visualDensity: VisualDensity.compact,
              iconSize: 18,
              color: const Color(0xFFC9A24A),
              icon: const Icon(Icons.more_horiz),
            ),
          ),
        if (state.hasLiveSensors)
          Positioned(
            top: -7,
            left: -8,
            child: IconButton(
              onPressed: onStats,
              tooltip: 'Stats',
              visualDensity: VisualDensity.compact,
              iconSize: 17,
              color: const Color(0xFFC9A24A),
              icon: const Icon(Icons.monitor_heart_outlined),
            ),
          ),
        if (connectionLost)
          Positioned(
            top: 2,
            left: 0,
            right: hasActions ? 30 : 0,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.link_off, size: 12, color: Color(0xFFFF4D5E)),
                const SizedBox(width: 3),
                Flexible(
                  child: Text(
                    'Connexion au téléphone perdue',
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    textAlign: TextAlign.center,
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      color: const Color(0xFFA7ADBA),
                      fontSize: 9,
                    ),
                  ),
                ),
              ],
            ),
          ),
        if (footerText.isNotEmpty)
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            child: Text(
              footerText,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              textAlign: TextAlign.center,
              style: Theme.of(
                context,
              ).textTheme.bodySmall?.copyWith(fontSize: 10),
            ),
          ),
      ],
    );
  }
}

final class _DominantValue extends StatelessWidget {
  const _DominantValue(this.value);

  final String value;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 48,
      child: FittedBox(
        fit: BoxFit.scaleDown,
        child: Text(
          value,
          maxLines: 1,
          textAlign: TextAlign.center,
          style: Theme.of(context).textTheme.displayLarge,
        ),
      ),
    );
  }
}

final class _SmallLabel extends StatelessWidget {
  const _SmallLabel(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      maxLines: 1,
      overflow: TextOverflow.ellipsis,
      textAlign: TextAlign.center,
      style: Theme.of(context).textTheme.labelSmall,
    );
  }
}

final class _ExerciseName extends StatelessWidget {
  const _ExerciseName(this.name);

  final String name;

  @override
  Widget build(BuildContext context) {
    return Text(
      name.isEmpty ? 'Séance en cours' : name,
      maxLines: 2,
      overflow: TextOverflow.ellipsis,
      textAlign: TextAlign.center,
      style: Theme.of(context).textTheme.titleSmall,
    );
  }
}

final class _StepNameBand extends StatelessWidget {
  const _StepNameBand(this.name);

  final String? name;

  @override
  Widget build(BuildContext context) {
    final value = name?.trim();
    if (value == null || value.isEmpty) {
      return const SizedBox.shrink();
    }
    return Container(
      width: double.infinity,
      margin: const EdgeInsets.only(top: 5, bottom: 6),
      padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 4),
      alignment: Alignment.center,
      decoration: BoxDecoration(
        color: const Color(0xFF141824),
        border: Border.all(color: const Color(0xFF414754)),
        borderRadius: BorderRadius.circular(6),
      ),
      child: Text(
        value,
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
        textAlign: TextAlign.center,
        style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 10),
      ),
    );
  }
}

final class _StatusLine extends StatelessWidget {
  const _StatusLine(this.text);

  final String? text;

  @override
  Widget build(BuildContext context) {
    final value = text;
    if (value == null || value.isEmpty) {
      return const SizedBox(height: 13);
    }
    return SizedBox(
      height: 13,
      child: Text(
        value,
        maxLines: 1,
        overflow: TextOverflow.ellipsis,
        textAlign: TextAlign.center,
        style: Theme.of(context).textTheme.bodySmall,
      ),
    );
  }
}

final class _MainHeartRateLine extends StatelessWidget {
  const _MainHeartRateLine({required this.sample});

  final WatchSensorSample? sample;

  @override
  Widget build(BuildContext context) {
    final label = _heartRateLabel(sample);
    if (label == null) {
      return const SizedBox.shrink();
    }
    return Padding(
      padding: const EdgeInsets.only(top: 2),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.favorite, size: 11, color: Color(0xFFFF4D5E)),
          const SizedBox(width: 3),
          Text(
            label,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.bodySmall?.copyWith(
              color: const Color(0xFFA7ADBA),
              fontSize: 11,
            ),
          ),
        ],
      ),
    );
  }
}

final class _ScaledContent extends StatelessWidget {
  const _ScaledContent({required this.child});

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        return Center(
          child: FittedBox(
            fit: BoxFit.scaleDown,
            child: SizedBox(width: constraints.maxWidth, child: child),
          ),
        );
      },
    );
  }
}

final class _ActiveContent extends StatelessWidget {
  const _ActiveContent({
    required this.state,
    required this.onTogglePause,
    required this.onIncrementScore,
    required this.onDecrementScore,
    required this.onCompleteStep,
    required this.nowEpochMs,
  });

  final WatchSessionUiState state;
  final VoidCallback? onTogglePause;
  final VoidCallback onIncrementScore;
  final VoidCallback onDecrementScore;
  final VoidCallback onCompleteStep;
  final int nowEpochMs;

  @override
  Widget build(BuildContext context) {
    final projection = state.projection;
    if (projection.hasManualScore) {
      return _ManualScoreContent(
        state: state,
        onTogglePause: onTogglePause,
        onIncrement: onIncrementScore,
        onDecrement: onDecrementScore,
        nowEpochMs: nowEpochMs,
      );
    }
    final timer = _primaryDisplayTimer(projection);
    final secondaryTimers = _visibleSecondaryTimers(
      projection,
      primaryTimer: timer,
    );
    final repsTarget = _repsStepTarget(projection);
    final dominantValue = timer == null
        ? _seriesValue(projection)
        : _timerText(timer, nowEpochMs: nowEpochMs);
    final dominantLabel = timer == null ? 'SÉRIE' : timer.label;
    final controlsEnabled =
        !state.connectionLost &&
        projection.phoneReachable &&
        state.actionsEnabled;
    return _ScaledContent(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _ExerciseName(projection.exerciseName),
          _StepNameBand(projection.stepName),
          const SizedBox(height: 2),
          _SmallLabel(repsTarget == null ? dominantLabel : 'Répétitions'),
          const SizedBox(height: 2),
          if (repsTarget != null)
            _DominantRepsLine(
              value: repsTarget.toString(),
              pending: state.commandPending,
              onComplete: controlsEnabled ? onCompleteStep : null,
            )
          else if (timer == null)
            _DominantValue(dominantValue)
          else
            _DominantTimerLine(
              value: dominantValue,
              timer: timer,
              pending: state.timerTogglePending,
              onTogglePause: onTogglePause,
            ),
          _MainHeartRateLine(sample: state.sensorSample),
          if (timer == null) ...[
            const SizedBox(height: 5),
            _StatusLine(projection.statusLabel),
          ],
          if (secondaryTimers.isNotEmpty)
            Padding(
              padding: const EdgeInsets.only(top: 4),
              child: Text(
                secondaryTimers
                    .map(
                      (timer) =>
                          _compactTimerText(timer, nowEpochMs: nowEpochMs),
                    )
                    .join(' · '),
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.bodySmall,
              ),
            ),
        ],
      ),
    );
  }
}

final class _DominantRepsLine extends StatelessWidget {
  const _DominantRepsLine({
    required this.value,
    required this.pending,
    required this.onComplete,
  });

  final String value;
  final bool pending;
  final VoidCallback? onComplete;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 52,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Flexible(child: _DominantValue(value)),
          const SizedBox(width: 6),
          _CompleteStepButton(pending: pending, onPressed: onComplete),
        ],
      ),
    );
  }
}

final class _CompleteStepButton extends StatelessWidget {
  const _CompleteStepButton({required this.pending, required this.onPressed});

  final bool pending;
  final VoidCallback? onPressed;

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: 48,
      child: IconButton(
        onPressed: onPressed,
        tooltip: 'Valider l’étape',
        visualDensity: VisualDensity.compact,
        iconSize: 24,
        color: const Color(0xFFC9A24A),
        disabledColor: const Color(0xFF414754),
        style: IconButton.styleFrom(
          backgroundColor: const Color(0xFF141824),
          side: const BorderSide(color: Color(0xFFD72638)),
        ),
        icon: pending
            ? const _PendingDot(key: ValueKey('complete-step-pending-dot'))
            : const Icon(Icons.check),
      ),
    );
  }
}

final class _DominantTimerLine extends StatelessWidget {
  const _DominantTimerLine({
    required this.value,
    required this.timer,
    required this.pending,
    required this.onTogglePause,
    this.compact = false,
  });

  final String value;
  final WatchTimerProjection timer;
  final bool pending;
  final VoidCallback? onTogglePause;
  final bool compact;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: compact ? 34 : 48,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Flexible(
            child: compact
                ? Text(
                    value,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                    textAlign: TextAlign.center,
                    style: Theme.of(
                      context,
                    ).textTheme.titleSmall?.copyWith(fontSize: 26, height: 1),
                  )
                : _DominantValue(value),
          ),
          SizedBox(width: compact ? 4 : 6),
          _TimerToggleButton(
            runState: timer.runState,
            pending: pending,
            onPressed: onTogglePause,
            compact: compact,
          ),
        ],
      ),
    );
  }
}

final class _ManualScoreContent extends StatelessWidget {
  const _ManualScoreContent({
    required this.state,
    required this.onTogglePause,
    required this.onIncrement,
    required this.onDecrement,
    required this.nowEpochMs,
  });

  final WatchSessionUiState state;
  final VoidCallback? onTogglePause;
  final VoidCallback onIncrement;
  final VoidCallback onDecrement;
  final int nowEpochMs;

  @override
  Widget build(BuildContext context) {
    final projection = state.projection;
    final score =
        state.optimisticManualScoreValue ??
        projection.currentManualScoreValue ??
        0;
    final controlsEnabled = state.actionsEnabled && projection.phoneReachable;
    final canDecrement =
        controlsEnabled &&
        score > 0 &&
        (state.scoreCommandPending || projection.canDecrementScore);
    final timer = _primaryDisplayTimer(projection);
    final canToggleTimer =
        timer != null &&
        controlsEnabled &&
        !state.commandPending &&
        _timerButtonCommandMatches(
          timer: timer,
          primaryAction: projection.primaryAction,
        );
    final target = projection.manualScoreTargetValue;
    final targetLabel = projection.manualScoreTargetLabel;
    final captionSegments = [
      if (projection.manualScoreRepsTargetValue != null)
        'Répétitions : ${projection.manualScoreRepsTargetValue}',
      if (target != null && targetLabel != null && targetLabel.isNotEmpty)
        '$targetLabel : ${_scoreText(target)}',
    ];
    return _ScaledContent(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _ExerciseName(projection.exerciseName),
          _StepNameBand(projection.stepName),
          if (captionSegments.isNotEmpty) ...[
            Text(
              captionSegments.join(' · '),
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodySmall?.copyWith(
                color: const Color(0xFFA7ADBA),
                fontSize: 11,
              ),
            ),
            const SizedBox(height: 1),
          ],
          const _SmallLabel('SCORE'),
          const SizedBox(height: 2),
          SizedBox(
            height: 54,
            child: Row(
              children: [
                _ScoreButton(
                  label: '−',
                  onPressed: canDecrement ? onDecrement : null,
                ),
                Expanded(
                  child: Opacity(
                    opacity: state.scoreCommandPending ? 0.58 : 1,
                    child: _DominantValue(_scoreText(score)),
                  ),
                ),
                _ScoreButton(
                  label: '+',
                  onPressed: controlsEnabled ? onIncrement : null,
                ),
              ],
            ),
          ),
          SizedBox(
            height: 6,
            child: state.scoreWaitingForPhone
                ? const _PendingDot(key: ValueKey('score-pending-dot'))
                : const SizedBox.shrink(),
          ),
          _MainHeartRateLine(sample: state.sensorSample),
          const SizedBox(height: 5),
          if (timer != null)
            _CompactTimerLine(
              timer: timer,
              pending: state.timerTogglePending,
              onTogglePause: canToggleTimer ? onTogglePause : null,
              nowEpochMs: nowEpochMs,
            )
          else
            _StatusLine(projection.statusLabel),
        ],
      ),
    );
  }
}

final class _CompactTimerLine extends StatelessWidget {
  const _CompactTimerLine({
    required this.timer,
    required this.pending,
    required this.onTogglePause,
    required this.nowEpochMs,
  });

  final WatchTimerProjection timer;
  final bool pending;
  final VoidCallback? onTogglePause;
  final int nowEpochMs;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 45,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _SmallLabel(timer.label),
          const SizedBox(height: 1),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Flexible(
                child: Text(
                  _timerText(timer, nowEpochMs: nowEpochMs),
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  textAlign: TextAlign.center,
                  style: Theme.of(
                    context,
                  ).textTheme.titleSmall?.copyWith(fontSize: 18, height: 1),
                ),
              ),
              const SizedBox(width: 4),
              _TimerToggleButton(
                runState: timer.runState,
                pending: pending,
                onPressed: onTogglePause,
                compact: true,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

final class _ScoreButton extends StatelessWidget {
  const _ScoreButton({required this.label, required this.onPressed});

  final String label;
  final VoidCallback? onPressed;

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: 48,
      child: IconButton(
        onPressed: onPressed,
        tooltip: label == '+' ? 'Ajouter' : 'Retirer',
        visualDensity: VisualDensity.compact,
        iconSize: 24,
        color: const Color(0xFFC9A24A),
        disabledColor: const Color(0xFF414754),
        icon: Text(
          label,
          style: Theme.of(context).textTheme.titleSmall?.copyWith(
            color: onPressed == null
                ? const Color(0xFF414754)
                : const Color(0xFFC9A24A),
            fontSize: 24,
          ),
        ),
      ),
    );
  }
}

final class _TimerToggleButton extends StatelessWidget {
  const _TimerToggleButton({
    required this.runState,
    required this.pending,
    required this.onPressed,
    this.compact = false,
  });

  final WatchTimerRunState runState;
  final bool pending;
  final VoidCallback? onPressed;
  final bool compact;

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: compact ? 28 : 48,
      child: IconButton(
        onPressed: onPressed,
        tooltip: _timerButtonTooltip(runState),
        visualDensity: VisualDensity.compact,
        iconSize: compact ? 16 : 26,
        color: const Color(0xFFC9A24A),
        disabledColor: const Color(0xFF414754),
        style: IconButton.styleFrom(
          backgroundColor: const Color(0xFF141824),
          side: const BorderSide(color: Color(0xFFD72638)),
        ),
        icon: pending
            ? const _PendingDot(key: ValueKey('timer-toggle-pending-dot'))
            : Icon(_timerButtonIcon(runState)),
      ),
    );
  }
}

final class _PendingDot extends StatelessWidget {
  const _PendingDot({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: 5,
        height: 5,
        decoration: const BoxDecoration(
          color: Color(0xFFC9A24A),
          shape: BoxShape.circle,
        ),
      ),
    );
  }
}

final class _RestContent extends StatelessWidget {
  const _RestContent({
    required this.state,
    required this.onTogglePause,
    required this.nowEpochMs,
  });

  final WatchSessionUiState state;
  final VoidCallback? onTogglePause;
  final int nowEpochMs;

  @override
  Widget build(BuildContext context) {
    final projection = state.projection;
    final timer = _primaryDisplayTimer(projection);
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const _SmallLabel('REPOS'),
        const SizedBox(height: 2),
        _SmallLabel(_afterSeriesLabel(projection)),
        if (timer != null) ...[
          const SizedBox(height: 2),
          _DominantTimerLine(
            value: _timerText(timer, nowEpochMs: nowEpochMs),
            timer: timer,
            pending: state.timerTogglePending,
            onTogglePause: onTogglePause,
            compact: true,
          ),
        ],
        _MainHeartRateLine(sample: state.sensorSample),
        const SizedBox(height: 3),
        if (projection.nextExerciseName case final next?) ...[
          const _SmallLabel('Ensuite'),
          Text(
            next,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
            textAlign: TextAlign.center,
            style: Theme.of(context).textTheme.bodyMedium,
          ),
        ] else ...[
          _ExerciseName(projection.exerciseName),
        ],
      ],
    );
  }
}

final class _StatsView extends StatelessWidget {
  const _StatsView({required this.state, required this.onSession});

  final WatchSessionUiState state;
  final VoidCallback onSession;

  @override
  Widget build(BuildContext context) {
    final sample = state.sensorSample;
    final metrics = [
      _StatsMetric(
        icon: Icons.favorite,
        label: 'FC',
        value: _heartRateMetricLabel(sample),
        available: _heartRateLabel(sample) != null,
      ),
      _StatsMetric(
        icon: Icons.directions_run,
        label: 'Distance',
        value: _distanceMetricLabel(sample),
        available: _distanceLabel(sample) != null,
      ),
      _StatsMetric(
        icon: Icons.local_fire_department,
        label: 'Calories',
        value: _caloriesMetricLabel(sample),
        available: _caloriesLabel(sample) != null,
      ),
    ];
    return Stack(
      children: [
        Positioned(
          top: 0,
          left: 0,
          right: 36,
          child: Text(
            'Stats',
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: Theme.of(context).textTheme.titleSmall,
          ),
        ),
        Positioned(
          top: -8,
          right: -8,
          child: IconButton(
            onPressed: onSession,
            tooltip: 'Séance',
            visualDensity: VisualDensity.compact,
            icon: const Icon(Icons.chevron_left),
          ),
        ),
        Positioned.fill(
          top: 30,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              for (var index = 0; index < metrics.length; index += 1) ...[
                metrics[index],
                if (index < metrics.length - 1) const SizedBox(height: 6),
              ],
            ],
          ),
        ),
      ],
    );
  }
}

final class _StatsMetric extends StatelessWidget {
  const _StatsMetric({
    required this.icon,
    required this.label,
    required this.value,
    required this.available,
  });

  final IconData icon;
  final String label;
  final String value;
  final bool available;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
      decoration: BoxDecoration(
        color: const Color(0xFF141824),
        border: Border.all(color: const Color(0xFF414754)),
        borderRadius: BorderRadius.circular(6),
      ),
      child: Row(
        children: [
          Icon(icon, size: 14, color: const Color(0xFFC9A24A)),
          const SizedBox(width: 6),
          Expanded(
            child: Text(
              label,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ),
          Flexible(
            child: Text(
              value,
              maxLines: 1,
              overflow: TextOverflow.ellipsis,
              textAlign: TextAlign.end,
              style: Theme.of(context).textTheme.titleSmall?.copyWith(
                fontSize: available ? 14 : 11,
                color: available
                    ? const Color(0xFFC9A24A)
                    : const Color(0xFFA7ADBA),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

final class _ActionsView extends StatelessWidget {
  const _ActionsView({
    required this.state,
    required this.onAction,
    required this.onSession,
  });

  final WatchSessionUiState state;
  final ValueChanged<WatchSecondaryAction> onAction;
  final VoidCallback onSession;

  @override
  Widget build(BuildContext context) {
    final actions = state.projection.secondaryActions;
    final connectionLost =
        state.connectionLost || !state.projection.phoneReachable;
    if (actions.isEmpty || connectionLost) {
      return _EmptyActions(connectionLost: connectionLost);
    }
    final visibleActions = actions.take(5).toList();
    final buttonHeight = visibleActions.length > 3 ? 24.0 : 30.0;
    final buttonGap = visibleActions.length > 3 ? 4.0 : 6.0;
    return Stack(
      children: [
        Positioned(
          top: 0,
          left: 0,
          right: 36,
          child: Text(
            'Actions',
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: Theme.of(context).textTheme.titleSmall,
          ),
        ),
        Positioned(
          top: -8,
          right: -8,
          child: IconButton(
            onPressed: onSession,
            tooltip: 'Séance',
            visualDensity: VisualDensity.compact,
            icon: const Icon(Icons.chevron_left),
          ),
        ),
        Positioned.fill(
          top: 34,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              for (
                var index = 0;
                index < visibleActions.length;
                index += 1
              ) ...[
                SizedBox(
                  height: buttonHeight,
                  width: double.infinity,
                  child: OutlinedButton(
                    onPressed: state.actionsEnabled
                        ? () => onAction(visibleActions[index])
                        : null,
                    style: OutlinedButton.styleFrom(
                      minimumSize: Size.fromHeight(buttonHeight),
                      padding: const EdgeInsets.symmetric(horizontal: 8),
                      tapTargetSize: MaterialTapTargetSize.shrinkWrap,
                      visualDensity: VisualDensity.compact,
                    ),
                    child: _ActionButtonLabel(
                      _secondaryLabel(visibleActions[index]),
                    ),
                  ),
                ),
                if (index < visibleActions.length - 1)
                  SizedBox(height: buttonGap),
              ],
              if (actions.length > visibleActions.length)
                Padding(
                  padding: const EdgeInsets.only(top: 5),
                  child: Text(
                    '+${actions.length - visibleActions.length}',
                    textAlign: TextAlign.center,
                    style: Theme.of(context).textTheme.bodySmall,
                  ),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

final class _ActionButtonLabel extends StatelessWidget {
  const _ActionButtonLabel(this.text);

  final String text;

  @override
  Widget build(BuildContext context) {
    return FittedBox(
      fit: BoxFit.scaleDown,
      child: Text(text, maxLines: 1, textAlign: TextAlign.center),
    );
  }
}

final class _EmptyActions extends StatelessWidget {
  const _EmptyActions({required this.connectionLost});

  final bool connectionLost;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        connectionLost ? 'Connexion perdue' : 'Aucune action',
        maxLines: 2,
        overflow: TextOverflow.ellipsis,
        textAlign: TextAlign.center,
        style: Theme.of(context).textTheme.bodySmall,
      ),
    );
  }
}

final class _ConfirmSheet extends StatelessWidget {
  const _ConfirmSheet({
    required this.title,
    required this.message,
    required this.confirmLabel,
  });

  final String title;
  final String message;
  final String confirmLabel;

  @override
  Widget build(BuildContext context) {
    return Dialog.fullscreen(
      backgroundColor: const Color(0xFF080A12),
      child: Padding(
        padding: const EdgeInsets.all(18),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              title,
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.titleSmall,
            ),
            const SizedBox(height: 8),
            Text(
              message,
              maxLines: 2,
              overflow: TextOverflow.ellipsis,
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodySmall,
            ),
            const SizedBox(height: 14),
            FilledButton(
              onPressed: () => Navigator.of(context).pop(true),
              child: _ActionButtonLabel(confirmLabel),
            ),
            const SizedBox(height: 8),
            OutlinedButton(
              onPressed: () => Navigator.of(context).pop(false),
              child: const _ActionButtonLabel('Annuler'),
            ),
          ],
        ),
      ),
    );
  }
}

String _primaryLabel(WatchSessionUiState state) {
  if (state.commandPending) {
    return state.waitingForPhone ? 'En attente du téléphone' : 'Envoi...';
  }
  return switch (state.projection.primaryAction) {
    WatchPrimaryAction.none => 'Actualiser',
    WatchPrimaryAction.startCurrentExercise => 'Démarrer l’exercice',
    WatchPrimaryAction.pauseSession => 'Pause',
    WatchPrimaryAction.resumeSession => 'Reprendre',
    WatchPrimaryAction.startPreparedTimedStep => 'Démarrer le chrono',
    WatchPrimaryAction.skipCurrentRest => 'Passer le repos',
  };
}

String _secondaryLabel(WatchSecondaryAction action) {
  return switch (action) {
    WatchSecondaryAction.skipCurrentStep => 'Passer l’étape',
    WatchSecondaryAction.skipCurrentPassage => 'Passer le passage',
    WatchSecondaryAction.finishCurrentSet => 'Terminer la série',
    WatchSecondaryAction.skipCurrentSet => 'Passer la série',
    WatchSecondaryAction.skipCurrentRest => 'Passer le repos',
  };
}

bool _timerButtonCommandMatches({
  required WatchTimerProjection timer,
  required WatchPrimaryAction primaryAction,
}) {
  return switch (timer.runState) {
    WatchTimerRunState.stopped =>
      primaryAction == WatchPrimaryAction.startCurrentExercise ||
          primaryAction == WatchPrimaryAction.startPreparedTimedStep,
    WatchTimerRunState.running =>
      primaryAction == WatchPrimaryAction.pauseSession,
    WatchTimerRunState.paused =>
      primaryAction == WatchPrimaryAction.resumeSession,
  };
}

String _timerButtonTooltip(WatchTimerRunState runState) {
  return switch (runState) {
    WatchTimerRunState.stopped => 'Démarrer',
    WatchTimerRunState.running => 'Pause',
    WatchTimerRunState.paused => 'Reprendre',
  };
}

IconData _timerButtonIcon(WatchTimerRunState runState) {
  return switch (runState) {
    WatchTimerRunState.stopped => Icons.play_arrow,
    WatchTimerRunState.running => Icons.pause,
    WatchTimerRunState.paused => Icons.play_arrow,
  };
}

String? _contextLine(WatchSessionProjection projection) {
  final parts = [
    if (projection.passageIndex != null && projection.passageTotal != null)
      'Passage ${projection.passageIndex} / ${projection.passageTotal}',
    if (projection.stepIndex != null && projection.stepTotal != null)
      'Étape ${projection.stepIndex} / ${projection.stepTotal}',
  ];
  if (parts.isEmpty) {
    return null;
  }
  return parts.join(' · ');
}

String _seriesValue(WatchSessionProjection projection) {
  if (projection.seriesIndex <= 0 || projection.seriesTotal <= 0) {
    return '--';
  }
  return '${projection.seriesIndex}/${projection.seriesTotal}';
}

int? _repsStepTarget(WatchSessionProjection projection) {
  final target = projection.stepTargetValue;
  if (projection.stepType != WatchStepType.reps || target == null) {
    return null;
  }
  return target > 0 ? target : null;
}

String _scoreText(double value) {
  if (value == value.roundToDouble()) {
    return value.toInt().toString();
  }
  return value.toStringAsFixed(1);
}

String _exerciseProgress(WatchSessionProjection projection) {
  if (projection.passageIndex != null && projection.passageTotal != null) {
    return '${projection.passageIndex}/${projection.passageTotal}';
  }
  final contextLine = _contextLine(projection);
  if (contextLine != null) {
    return contextLine;
  }
  if (projection.seriesIndex > 0 && projection.seriesTotal > 0) {
    return 'Série ${projection.seriesIndex}/${projection.seriesTotal}';
  }
  return projection.statusLabel ?? '';
}

String _sessionFooterText(WatchSessionUiState state) {
  if (state.commandPending) {
    return _primaryLabel(state);
  }
  if (state.staleProjection) {
    return 'Dernier état reçu';
  }
  final projection = state.projection;
  if (projection.phase == WatchSessionPhase.restRunning ||
      projection.phase == WatchSessionPhase.restPaused) {
    return '';
  }
  return _exerciseProgress(projection);
}

String _afterSeriesLabel(WatchSessionProjection projection) {
  if (projection.seriesIndex <= 0 || projection.seriesTotal <= 0) {
    return 'Après série';
  }
  return 'Après série ${projection.seriesIndex}/${projection.seriesTotal}';
}

WatchTimerProjection? _primaryDisplayTimer(WatchSessionProjection projection) {
  final dominantTimer = projection.dominantTimer;
  if (dominantTimer != null && dominantTimer.kind != WatchTimerKind.setTimer) {
    return dominantTimer;
  }
  for (final timer in _visibleSecondaryTimers(projection)) {
    return timer;
  }
  return null;
}

List<WatchTimerProjection> _visibleSecondaryTimers(
  WatchSessionProjection projection, {
  WatchTimerProjection? primaryTimer,
}) {
  return projection.secondaryTimers
      .where(
        (timer) =>
            timer.kind != WatchTimerKind.setTimer && timer != primaryTimer,
      )
      .toList(growable: false);
}

bool _hasRunningVisibleTimer(WatchSessionProjection projection) {
  final primaryTimer = _primaryDisplayTimer(projection);
  if (primaryTimer?.runState == WatchTimerRunState.running) {
    return true;
  }
  return _visibleSecondaryTimers(
    projection,
    primaryTimer: primaryTimer,
  ).any((timer) => timer.runState == WatchTimerRunState.running);
}

String _timerText(WatchTimerProjection timer, {required int nowEpochMs}) {
  final duration = _displayDuration(timer, nowEpochMs: nowEpochMs);
  final totalSeconds = duration.inSeconds;
  final minutes = (totalSeconds ~/ 60).toString().padLeft(2, '0');
  final seconds = (totalSeconds % 60).toString().padLeft(2, '0');
  return '$minutes:$seconds';
}

String _compactTimerText(
  WatchTimerProjection timer, {
  required int nowEpochMs,
}) {
  return '${timer.label} ${_timerText(timer, nowEpochMs: nowEpochMs)}';
}

String _timerHapticKey(
  WatchSessionProjection projection,
  WatchTimerProjection timer,
) {
  return [
    projection.deviceSessionId,
    projection.seriesIndex,
    projection.passageIndex ?? '-',
    projection.stepIndex ?? '-',
    timer.kind.name,
    timer.label,
    timer.referenceEpochMs,
    timer.startedAtEpochMs ?? '-',
    timer.targetMs ?? '-',
  ].join(':');
}

String? _heartRateLabel(WatchSensorSample? sample) {
  final bpm = sample?.heartRateBpm;
  if (bpm == null || bpm <= 0) {
    return null;
  }
  return '$bpm bpm';
}

String _heartRateMetricLabel(WatchSensorSample? sample) {
  return _heartRateLabel(sample) ?? _sensorUnavailableLabel(sample);
}

String _distanceMetricLabel(WatchSensorSample? sample) {
  return _distanceLabel(sample) ?? _sensorUnavailableLabel(sample);
}

String _caloriesMetricLabel(WatchSensorSample? sample) {
  return _caloriesLabel(sample) ?? _sensorUnavailableLabel(sample);
}

String _sensorUnavailableLabel(WatchSensorSample? sample) {
  if (sample?.sensorCollectionStatus == 'authorizationRequired') {
    return 'Autorisation requise';
  }
  if (sample == null) {
    return 'Mesure en attente';
  }
  return 'Donnée indisponible';
}

String? _distanceLabel(WatchSensorSample? sample) {
  final meters = sample?.distanceMeters;
  if (meters == null || meters < 0) {
    return null;
  }
  if (meters >= 1000) {
    return '${(meters / 1000).toStringAsFixed(2)} km';
  }
  return '${meters.round()} m';
}

String? _caloriesLabel(WatchSensorSample? sample) {
  final calories = sample?.caloriesKcal;
  if (calories == null || calories < 0) {
    return null;
  }
  return '${calories.round()} kcal';
}

Duration _displayDuration(
  WatchTimerProjection timer, {
  required int nowEpochMs,
}) {
  final elapsedMs = _interpolatedElapsedMs(timer, nowEpochMs: nowEpochMs);
  final displayMs = switch (timer.displayMode) {
    WatchTimerDisplayMode.elapsed => elapsedMs,
    WatchTimerDisplayMode.countdown => (timer.targetMs ?? 0) - elapsedMs,
  };
  return Duration(milliseconds: displayMs < 0 ? 0 : displayMs);
}

int _interpolatedElapsedMs(
  WatchTimerProjection timer, {
  required int nowEpochMs,
}) {
  if (timer.runState != WatchTimerRunState.running ||
      timer.startedAtEpochMs == null) {
    return timer.accumulatedMs;
  }
  final elapsedSinceReference = (nowEpochMs - timer.referenceEpochMs).clamp(
    0,
    1 << 31,
  );
  return timer.accumulatedMs + elapsedSinceReference.toInt();
}
