# Skill — Timer Engine (useTimerEngine)
> Lis ce skill AVANT d'implémenter quoi que ce soit lié au timer.

## Responsabilité de ce module
Le timer est le cœur battant de l'app. Il doit être :
- **Précis** : drift < 50ms sur une séance complète (8 rounds)
- **Résilient** : continue en background, survit aux appels téléphoniques
- **Découplé** : aucune dépendance vers l'audio ou les exercices
- **Testable** : logique pure, facile à tester sans simulateur

---

## Architecture du module timer

```
src/features/timer/
  types.ts                    ← Commencer ICI
  hooks/
    useTimerEngine.ts         ← Moteur central (logique pure)
    useTimerSync.ts           ← Pont vers audio + exercices
    useTimerPersistence.ts    ← Sauvegarde config dans AsyncStorage
  components/
    TimerDisplay.tsx          ← Écran plein écran (props-only)
    TimerControls.tsx         ← Boutons start/pause/stop/skip
    TimerPhaseIndicator.tsx   ← Barre de progression des rounds
    TimerRing.tsx             ← Anneau circulaire animé
  index.ts                    ← Barrel export
```

---

## Types — commencer par ici

```typescript
// src/features/timer/types.ts

export type TimerPhase = 'IDLE' | 'GET_READY' | 'WORK' | 'REST' | 'COMPLETE'

export interface TimerConfig {
  workDuration: number        // défaut : 20s
  restDuration: number        // défaut : 10s
  rounds: number              // défaut : 8
  getReadyDuration: number    // défaut : 10s
  cycles: number              // défaut : 1 (Premium : jusqu'à 10)
  cyclePauseDuration: number  // défaut : 60s (pause entre cycles, Premium)
}

export interface TimerState {
  phase: TimerPhase
  secondsLeft: number
  currentRound: number        // 1-indexé
  totalRounds: number
  currentCycle: number        // 1-indexé
  totalCycles: number
  isRunning: boolean
  isPaused: boolean
  totalElapsedSeconds: number // pour le résumé de séance
}

export interface TimerActions {
  start:  (config: TimerConfig) => void
  pause:  () => void
  resume: () => void
  stop:   () => void
  skip:   () => void          // passer à la phase suivante immédiatement
}

// Événements émis par le timer — consommés par useTimerSync
export type TimerEvent =
  | { type: 'PHASE_CHANGED';    from: TimerPhase; to: TimerPhase }
  | { type: 'ROUND_COMPLETED';  round: number }
  | { type: 'SESSION_COMPLETE'; totalSeconds: number }
  | { type: 'COUNTDOWN_TICK';   secondsLeft: number }  // pour les dernières 3s

export type TimerEventListener = (event: TimerEvent) => void
```

---

## Implémentation — useTimerEngine

### Principe de précision : Date.now() delta

**JAMAIS** se fier uniquement à setInterval pour le compte à rebours.
setInterval drift de 10-100ms par tick sur mobile. Sur 8 rounds
de 20s + 10s, ça peut représenter plusieurs secondes d'écart.

```typescript
// ✅ Approche correcte — delta sur Date.now()
const tickRef = useRef<number | null>(null)
const targetEndTimeRef = useRef<number>(0)

function tick() {
  const now = Date.now()
  const remaining = Math.max(0, targetEndTimeRef.current - now)
  const secondsLeft = Math.ceil(remaining / 1000)

  setSecondsLeft(secondsLeft)

  if (remaining <= 0) {
    advancePhase()
  } else {
    // Planifier le prochain tick dans ~100ms (pas 1000ms)
    // pour une réactivité maximale sur les transitions
    tickRef.current = setTimeout(tick, 100)
  }
}

function startPhase(duration: number) {
  targetEndTimeRef.current = Date.now() + duration * 1000
  tickRef.current = setTimeout(tick, 100)
}
```

### Gestion du background (AppState)

```typescript
useEffect(() => {
  const subscription = AppState.addEventListener('change', (nextState) => {
    if (nextState === 'background' || nextState === 'inactive') {
      // Sauvegarder le timestamp cible — pas l'état courant
      // Quand on revient en foreground, on recalcule
      saveBackgroundTimestamp(targetEndTimeRef.current)
    }

    if (nextState === 'active') {
      const savedTarget = loadBackgroundTimestamp()
      if (savedTarget && isRunningRef.current) {
        // Recalculer le temps restant depuis le timestamp sauvegardé
        targetEndTimeRef.current = savedTarget
        // Si le temps est dépassé, avancer aux phases manquées
        reconcilePhaseAfterBackground(savedTarget, Date.now())
      }
    }
  })

  return () => subscription.remove()
}, [])
```

### Notification sticky pendant la séance

Utiliser expo-notifications pour afficher le décompte en background :

```typescript
async function updateBackgroundNotification(phase: TimerPhase, secondsLeft: number) {
  await Notifications.scheduleNotificationAsync({
    identifier: 'tabata-timer-sticky',
    content: {
      title: phase === 'WORK' ? '🔥 Travail !' : '💨 Repos',
      body: `${secondsLeft}s — Round ${currentRound}/${totalRounds}`,
      sticky: true,
      autoDismiss: false,
    },
    trigger: null,
  })
}
```

### Ordre des transitions de phase

```
IDLE
  ↓ start()
GET_READY (getReadyDuration)
  ↓ temps écoulé
WORK (workDuration)
  ↓ temps écoulé
REST (restDuration)
  ↓ temps écoulé
    → si currentRound < totalRounds : retour à WORK
    → si currentRound === totalRounds ET currentCycle < totalCycles :
        PAUSE_BETWEEN_CYCLES → WORK (round 1 du cycle suivant)
    → si currentRound === totalRounds ET currentCycle === totalCycles :
COMPLETE
  ↓ stop() ou auto-reset après 3s
IDLE
```

### Événements à émettre aux abonnés

```typescript
// À chaque changement de phase
emitEvent({ type: 'PHASE_CHANGED', from: prevPhase, to: nextPhase })

// À chaque fin de round
emitEvent({ type: 'ROUND_COMPLETED', round: currentRound })

// Décompte final (3, 2, 1)
if (secondsLeft <= 3) {
  emitEvent({ type: 'COUNTDOWN_TICK', secondsLeft })
}

// Fin de séance
emitEvent({ type: 'SESSION_COMPLETE', totalSeconds: totalElapsedSeconds })
```

---

## Interface publique du hook

```typescript
export function useTimerEngine(): TimerState & TimerActions & {
  addEventListener: (listener: TimerEventListener) => () => void
  config: TimerConfig
}
```

Le retour doit permettre :
```typescript
const {
  phase, secondsLeft, currentRound, totalRounds, isRunning, isPaused,
  start, pause, resume, stop, skip,
  addEventListener,
  config,
} = useTimerEngine()
```

---

## Composant TimerDisplay — layout plein écran

```
┌─────────────────────────────┐  ← StatusBar cachée
│  [Exercice]  Round 3/8  ●○○ │  ← Zone haute (20%) — fond semi-transparent
├─────────────────────────────┤
│                             │
│                             │
│         ┌───────┐           │
│         │  :17  │           │  ← Anneau + chiffre (50% de l'écran)
│         └───────┘           │  ← Police monospace, taille ~120px
│                             │
│  ████████████░░░░░░░░░░░░░  │  ← Barre de progression totale (15%)
│                             │
│   ⏸  ■  ⏭                  │  ← Boutons discrets (15%) — Pause/Stop/Skip
└─────────────────────────────┘
```

Props obligatoires du composant :
```typescript
interface TimerDisplayProps {
  state: TimerState
  exerciseName: string
  nextExerciseName: string
  onPause:  () => void
  onStop:   () => void
  onSkip:   () => void
}
```

### Animations requises
- **Fond** : transition de couleur animée entre phases (600ms ease-in-out)
  - `Animated.timing` sur la backgroundColor avec interpolation de couleurs
- **Chiffre** : légère pulsation à chaque seconde (`Animated.sequence` scale 1→1.05→1)
- **Anneau** : `Animated.Value` sur stroke-dashoffset (SVG) ou rotation
- **Décompte final** : flash rouge sur les 3 dernières secondes

```typescript
// Interpolation de couleur entre phases
const backgroundAnim = useRef(new Animated.Value(0)).current

const backgroundColor = backgroundAnim.interpolate({
  inputRange:  [0, 1, 2, 3],
  outputRange: [
    PHASE_COLORS.IDLE,
    PHASE_COLORS.GET_READY,
    PHASE_COLORS.WORK,
    PHASE_COLORS.REST,
  ],
})

// Déclencher quand la phase change
useEffect(() => {
  const phaseIndex = { IDLE: 0, GET_READY: 1, WORK: 2, REST: 3, COMPLETE: 0 }
  Animated.timing(backgroundAnim, {
    toValue: phaseIndex[phase],
    duration: 600,
    useNativeDriver: false, // obligatoire pour backgroundColor
  }).start()
}, [phase])
```

---

## expo-keep-awake — écran allumé pendant la séance

```typescript
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'

// Dans useTimerEngine
useEffect(() => {
  if (isRunning) {
    activateKeepAwakeAsync('tabata-session')
  } else {
    deactivateKeepAwake('tabata-session')
  }
}, [isRunning])
```

---

## Tests minimaux à écrire

```typescript
describe('useTimerEngine', () => {
  it('démarre en phase IDLE avec la config par défaut', () => {})
  it('passe à GET_READY quand start() est appelé', () => {})
  it('passe à WORK après la fin de GET_READY', () => {})
  it('passe à REST après la fin de WORK', () => {})
  it('incrémente currentRound après chaque REST', () => {})
  it('passe à COMPLETE après le dernier round', () => {})
  it('retourne à IDLE après stop()', () => {})
  it('skip() avance immédiatement à la phase suivante', () => {})
  it('émet PHASE_CHANGED à chaque transition', () => {})
  it('émet COUNTDOWN_TICK pour 3, 2, 1', () => {})
  it('cleanup useEffect enlève tous les listeners', () => {})
})
```

---

## Erreurs classiques à éviter

```typescript
// ❌ Drift — setInterval seul
setInterval(() => setSecondsLeft(prev => prev - 1), 1000)

// ✅ Delta sur Date.now()
const remaining = Math.max(0, targetEndTimeRef.current - Date.now())
setSecondsLeft(Math.ceil(remaining / 1000))

// ❌ State React pour les valeurs temps-critique
const [targetEndTime, setTargetEndTime] = useState(0)

// ✅ Ref pour les valeurs utilisées dans les closures de timeout
const targetEndTimeRef = useRef(0)

// ❌ Pas de cleanup — memory leak
useEffect(() => {
  setTimeout(tick, 100)
})

// ✅ Cleanup systématique
useEffect(() => {
  return () => {
    if (tickRef.current) clearTimeout(tickRef.current)
  }
}, [])
```
