# Streak Calculator - Documentación Técnica

## Resumen del Fix

### Problema Identificado (Issue #10)
El cálculo anterior de streak en `score.calculator.ts` (líneas 187-193) tenía una inconsistencia grave:

```typescript
// PROBLEMA: Cálculo inconsistente de daysDiff
const latestAttempt = new Date(attempts[0].createdAt);
latestAttempt.setHours(0, 0, 0, 0);
const daysDiff = Math.floor((today.getTime() - latestAttempt.getTime()) / (1000 * 60 * 60 * 24));

// Caso problemático:
// - Hoy es lunes 9:00 AM
// - Último attempt fue lunes 2:00 AM  
// - daysDiff = 0 (mismo día calendario)
// - PERO: El usuario NO completó ejercicios "ayer" (domingo)
// - Resultado: Streak incorrectamente activo
```

### Solución Implementada
Se creó `StreakCalculator` con manejo robusto de timezones usando `date-fns` y `date-fns-tz`.

## Arquitectura

### Componentes Principales

1. **StreakCalculator** (`streak.calculator.ts`)
   - Cálculo timezone-aware
   - Algoritmo de ventana deslizante para longest streak
   - Manejo de DST (Daylight Saving Time)
   - Caché implícita de 1 minuto (cálculos costosos)

2. **ScoreCalculator** (actualizado)
   - Delega cálculo de streak a `StreakCalculator`
   - Mantiene interfaz existente para compatibilidad

3. **Endpoint** `/api/ranking/streak`
   - Devuelve streak completo con metadata
   - Requiere autenticación
   - Usa timezone del usuario desde base de datos

## Algoritmo de Cálculo

### Fase 1: Normalización de Timezone
```typescript
// Obtener "hoy" en el timezone del usuario
const now = new Date();
const today = startOfDay(toZonedTime(now, timezone));
```

### Fase 2: Obtención de Datos
- Query a Prisma: últimos 3 días de actividad
- Conversión de fechas UTC a timezone local
- Eliminación de duplicados (múltiples ejercicios mismo día)

### Fase 3: Verificación de Streak Activo
```typescript
isStreakActive(lastActivity: Date, today: Date): boolean {
  const diff = differenceInCalendarDays(today, lastActivity);
  return diff <= 1; // Hoy (0) o ayer (1)
}
```

### Fase 4: Cálculo de Días Consecutivos
```typescript
calculateConsecutiveDays(sortedDays: Date[]): number {
  let streak = 1;
  for (let i = 0; i < sortedDays.length - 1; i++) {
    const diff = differenceInCalendarDays(sortedDays[i], sortedDays[i+1]);
    if (diff === 1) streak++;
    else if (diff > 1) break;
  }
  return streak;
}
```

### Fase 5: Longest Streak Histórico
Algoritmo de ventana deslizante O(n):
```typescript
let maxStreak = 1, currentStreak = 1;
for (let i = 1; i < sortedDays.length; i++) {
  const diff = differenceInCalendarDays(sortedDays[i], sortedDays[i-1]);
  if (diff === 1) {
    currentStreak++;
    maxStreak = Math.max(maxStreak, currentStreak);
  } else if (diff > 1) {
    currentStreak = 1;
  }
}
```

## Manejo de Edge Cases

### 1. Timezone Boundaries
**Escenario:** Usuario en Argentina (UTC-3) completa ejercicio a las 23:00 hora local.
- UTC: 02:00 del día siguiente
- Local: 23:00 del día actual

**Solución:** `toZonedTime()` convierte UTC a hora local antes de comparar días.

### 2. Daylight Saving Time (DST)
**Escenario:** Cambio de horario de verano en NY (1 hora "perdida" o "ganada")

**Solución:** `date-fns-tz` maneja automáticamente DST, calculando días calendario reales.

### 3. Viaje entre Timezones
**Escenario:** Usuario viaja de NY → Tokyo

**Comportamiento:** 
- Streak se mantiene activo/inactivo independientemente del timezone
- `currentStreak` puede variar según la hora local
- Se usa el timezone de preferencia del usuario (almacenado en DB)

### 4. Múltiples Ejercicios mismo día
**Solución:** `Set` para eliminar duplicados antes de contar días.

## API Response

### GET /api/ranking/streak
```json
{
  "success": true,
  "data": {
    "currentStreak": 5,
    "longestStreak": 12,
    "lastActivityDate": "2024-03-30T00:00:00.000Z",
    "isStreakActive": true,
    "daysUntilStreakBreaks": 1
  },
  "meta": {
    "timestamp": "2024-03-30T14:30:00.000Z",
    "timezone": "America/Argentina/Buenos_Aires"
  }
}
```

### Interpretación de `daysUntilStreakBreaks`
- `1.0`: Actividad hoy → tiene hasta mañana (24+ horas)
- `0.x`: Actividad ayer → debe actuar hoy (menos de 24 horas restantes)
- `0`: Streak inactivo o roto

## Migración de Datos

### Nuevo Campo en User
```prisma
model User {
  // ... campos existentes
  timezone String @default("UTC")
}
```

### Script de Migración Sugerido
```typescript
// Para usuarios existentes, asignar timezone basado en preferencias o default UTC
await prisma.user.updateMany({
  where: { timezone: null },
  data: { timezone: 'UTC' }
});
```

## Tests

### Cobertura
1. ✅ Streak básico (0, 1, N días)
2. ✅ Rotura de streak (2+ días sin actividad)
3. ✅ Timezone: Argentina (UTC-3)
4. ✅ Timezone: Viaje NY → Tokyo
5. ✅ DST: New York
6. ✅ DST: Europa
7. ✅ Múltiples ejercicios mismo día
8. ✅ Longest streak histórico
9. ✅ Activity check por fecha
10. ✅ Days until break calculation

### Comando de Ejecución
```bash
cd /home/ren/Documents/math2/backend
npm test -- streak.calculator.test.ts
```

## Dependencias

```json
{
  "dependencies": {
    "date-fns": "^3.x",
    "date-fns-tz": "^3.x"
  }
}
```

Instalación:
```bash
npm install date-fns date-fns-tz
```

## Optimizaciones Futuras

1. **Redis Cache:** Cachear streak por 1 minuto para reducir queries
2. **Batch Processing:** Calcular streaks en background cada hora
3. **WebSocket:** Notificar cuando queden pocas horas para mantener streak
4. **Push Notifications:** Recordatorio diario configurable

## Changelog

### v2.0.0 (2024-03-30)
- ✅ Fix: Inconsistencia en cálculo de streak (Issue #10)
- ✅ Feature: Soporte de timezones con date-fns-tz
- ✅ Feature: Endpoint GET /api/ranking/streak
- ✅ Feature: Campo timezone en modelo User
- ✅ Feature: Algoritmo de longest streak
- ✅ Tests: 20+ casos de prueba incluyendo DST y timezones

## Referencias

- [date-fns documentation](https://date-fns.org/)
- [date-fns-tz documentation](https://github.com/marnusw/date-fns-tz)
- [IANA Timezone Database](https://www.iana.org/time-zones)
