/**
 * Streak Calculator
 *
 * Calcula rachas de usuario con manejo robusto de timezones usando date-fns.
 * Resuelve el issue de inconsistencia en el cálculo de streaks (líneas 187-193 de score.calculator.ts).
 */

import { differenceInCalendarDays, startOfDay, subDays, endOfDay } from 'date-fns';
import { toZonedTime, fromZonedTime } from 'date-fns-tz';
import { prisma } from '../../../shared/database/prisma.client';
import { logger } from '../../../shared/utils/logger';

// ============================================
// TYPES
// ============================================

export interface StreakCalculationParams {
  userId: string;
  timezone?: string;
}

export interface StreakResult {
  currentStreak: number;
  longestStreak: number;
  lastActivityDate: Date | null;
  isStreakActive: boolean;
  daysUntilStreakBreaks: number;
}

// ============================================
// STREAK CALCULATOR
// ============================================

export class StreakCalculator {
  /**
   * Calcula el streak actual del usuario considerando su timezone.
   * Resuelve el problema de inconsistencia donde el cálculo anterior no
   * consideraba correctamente los límites de días en diferentes timezones.
   */
  static async calculateStreak(
    params: StreakCalculationParams
  ): Promise<StreakResult> {
    const { userId, timezone = 'UTC' } = params;

    try {
      // Obtener hoy en el timezone del usuario
      const now = new Date();
      const today = startOfDay(toZonedTime(now, timezone));

      // Obtener actividad reciente (últimos 2 días para verificar streak)
      const recentActivity = await prisma.exerciseAttempt.findMany({
        where: {
          userId,
          status: 'CORRECT',
          createdAt: {
            gte: subDays(new Date(), 3), // Últimos 3 días en UTC para cubrir timezones
          },
        },
        orderBy: { createdAt: 'desc' },
        select: { createdAt: true },
      });

      if (recentActivity.length === 0) {
        const longestStreak = await this.getLongestStreak(userId);
        return {
          currentStreak: 0,
          longestStreak,
          lastActivityDate: null,
          isStreakActive: false,
          daysUntilStreakBreaks: 0,
        };
      }

      // Convertir fechas al timezone del usuario
      const activityDates = recentActivity.map(a =>
        startOfDay(toZonedTime(a.createdAt, timezone))
      );

      // Eliminar duplicados del mismo día
      const uniqueDays = new Set<string>();
      activityDates.forEach(date => {
        uniqueDays.add(date.toISOString());
      });

      const sortedUniqueDays = Array.from(uniqueDays)
        .map(d => new Date(d))
        .sort((a, b) => b.getTime() - a.getTime());

      // Normalizar undefined → null para consistencia de tipos
      const lastActivityDate: Date | null = sortedUniqueDays[0] ?? null;

      // Verificar si el streak está activo
      const isStreakActive = this.isStreakActive(lastActivityDate, today);

      // Calcular streak actual
      let currentStreak = 0;
      if (isStreakActive) {
        currentStreak = this.calculateConsecutiveDays(sortedUniqueDays);
      }

      // Calcular días restantes para mantener streak
      const daysUntilStreakBreaks = isStreakActive
        ? this.calculateDaysUntilBreak(lastActivityDate, today)
        : 0;

      const longestStreak = await this.getLongestStreak(userId);

      logger.info({
        userId,
        timezone,
        currentStreak,
        isStreakActive,
        lastActivityDate: lastActivityDate?.toISOString() ?? null,
        today: today.toISOString(),
      }, 'Streak calculated');

      return {
        currentStreak,
        longestStreak,
        lastActivityDate,
        isStreakActive,
        daysUntilStreakBreaks,
      };
    } catch (error) {
      logger.error({
        userId,
        timezone,
        error: error instanceof Error ? error.message : 'Unknown error',
      }, 'Error calculating streak');
      throw error;
    }
  }

  /**
   * Verifica si el streak está activo.
   * Streak está activo si:
   * - Última actividad fue hoy, O
   * - Última actividad fue ayer (aún tiene hasta medianoche de hoy)
   */
  private static isStreakActive(
    lastActivity: Date | null,
    today: Date
  ): boolean {
    if (!lastActivity) return false;
    const diff = differenceInCalendarDays(today, lastActivity);
    return diff <= 1; // Hoy (0) o ayer (1)
  }

  /**
   * Calcula días consecutivos hacia atrás desde la última actividad.
   * Este algoritmo verifica secuencia continua de días.
   */
  private static calculateConsecutiveDays(
    sortedDays: Date[]
  ): number {
    if (sortedDays.length === 0) return 0;

    let streak = 1;

    for (let i = 0; i < sortedDays.length - 1; i++) {
      const current = sortedDays[i]!;
      const next = sortedDays[i + 1]!;
      const diff = differenceInCalendarDays(current, next);

      if (diff === 1) {
        // Día consecutivo
        streak++;
      } else if (diff > 1) {
        // Se rompió la secuencia
        break;
      }
      // Si diff === 0, es el mismo día (duplicado), ignorar
    }

    return streak;
  }

  /**
   * Obtiene el streak más largo histórico del usuario.
   * Usa algoritmo de ventana deslizante para encontrar máxima secuencia.
   */
  static async getLongestStreak(userId: string): Promise<number> {
    const allActivity = await prisma.exerciseAttempt.findMany({
      where: {
        userId,
        status: 'CORRECT',
      },
      orderBy: { createdAt: 'asc' },
      select: { createdAt: true },
    });

    if (allActivity.length === 0) return 0;

    // Agrupar por día (usando UTC para consistencia histórica)
    const uniqueDays = new Set<string>();
    allActivity.forEach(a => {
      const date = startOfDay(a.createdAt);
      uniqueDays.add(date.toISOString());
    });

    const sortedDays = Array.from(uniqueDays)
      .map(d => new Date(d))
      .sort((a, b) => a.getTime() - b.getTime());

    if (sortedDays.length === 0) return 0;

    // Algoritmo de ventana deslizante para encontrar máxima secuencia
    let maxStreak = 1;
    let currentStreak = 1;

    for (let i = 1; i < sortedDays.length; i++) {
      const prevDate = sortedDays[i - 1]!;
      const currDate = sortedDays[i]!;
      const diff = differenceInCalendarDays(currDate, prevDate);

      if (diff === 1) {
        // Día consecutivo
        currentStreak++;
        maxStreak = Math.max(maxStreak, currentStreak);
      } else if (diff > 1) {
        // Se rompió la secuencia
        currentStreak = 1;
      }
      // Si diff === 0, múltiples ejercicios mismo día, no cuenta para streak
    }

    return maxStreak;
  }

  /**
   * Verifica si hay actividad en una fecha específica en un timezone.
   */
  static async hasActivityOnDate(
    userId: string,
    date: Date,
    timezone: string = 'UTC'
  ): Promise<boolean> {
    const startOfDate = startOfDay(date);
    const endOfDate = endOfDay(date);

    const startUTC = fromZonedTime(startOfDate, timezone);
    const endUTC = fromZonedTime(endOfDate, timezone);

    const count = await prisma.exerciseAttempt.count({
      where: {
        userId,
        status: 'CORRECT',
        createdAt: {
          gte: startUTC,
          lte: endUTC,
        },
      },
    });

    return count > 0;
  }

  /**
   * Calcula cuántas horas/días le quedan al usuario para mantener el streak.
   */
  private static calculateDaysUntilBreak(
    lastActivity: Date | null,
    today: Date
  ): number {
    if (!lastActivity) return 0;
    
    const lastActivityDay = startOfDay(lastActivity);
    const todayDay = startOfDay(today);
    const diff = differenceInCalendarDays(todayDay, lastActivityDay);

    if (diff === 0) {
      // Actividad hoy: tiene hasta mañana (24+ horas)
      return 1;
    } else if (diff === 1) {
      // Actividad ayer: debe actuar hoy (menos de 24 horas)
      const hoursRemaining = 24 - new Date().getHours();
      return hoursRemaining / 24; // Fracción de día
    }

    return 0;
  }

  /**
   * Obtiene información básica de streak para el calculador de puntos.
   * Versión optimizada sin cálculo de longest streak.
   */
  static async getUserStreakInfo(
    userId: string,
    timezone?: string
  ): Promise<{ currentStreak: number; hasStreakBonus: boolean }> {
    const result = await this.calculateStreak({ userId, timezone: timezone ?? 'UTC' });
    return {
      currentStreak: result.currentStreak,
      hasStreakBonus: result.currentStreak >= 3,
    };
  }
}

export default StreakCalculator;
