/**
 * Progress Service
 *
 * Business logic for tracking user progress across modules
 * including statistics, completion status, and achievements
 */

import { prisma } from '../../shared/database/prisma.client';
import { NotFoundError } from '../../shared/types';
import { logger } from '../../shared/utils/logger';
import type { ModuleType } from '@prisma/client';
import type {
  ModuleProgress,
  RankingEntry,
} from '../../shared/types';

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

export interface OverallProgress {
  totalPoints: number;
  totalExercisesCompleted: number;
  totalModulesCompleted: number;
  totalModulesStarted: number;
  averageScore: number;
  totalTimeSpent: number; // in seconds
  perfectExercises: number;
  totalAttempts: number;
  currentStreak: number;
  modules: ModuleProgress[];
  ranking?: {
    global?: RankingEntry;
  };
}

export interface ModuleProgressDetail extends ModuleProgress {
  exercisesRemaining: number;
  averagePointsPerExercise: number;
  timeSpentHours: number;
  nextExercise?: {
    id: string;
    order: number;
    difficulty: string;
  };
  lastAttempt?: {
    exerciseId: string;
    status: string;
    createdAt: Date;
  };
}

export interface ProgressUpdate {
  exercisesCompleted?: number;
  points?: number;
  percentage?: number;
  isCompleted?: boolean;
  completedAt?: Date;
  perfectExercises?: number;
  attemptsCount?: number;
  totalTimeSpent?: number;
  averageScore?: number;
}

// ============================================
// SERVICE CLASS
// ============================================

class ProgressService {
  /**
   * Update progress from an exercise attempt
   * Called after a successful exercise attempt to decouple exercise service from progress logic
   */
  async updateFromAttempt(data: {
    userId: string;
    moduleId: string;
    exerciseId: string;
    isCorrect: boolean;
    isPartial: boolean;
    pointsEarned: number;
    timeSpentSeconds: number;
    isPerfect: boolean;
  }): Promise<void> {
    const { userId, moduleId, isCorrect, pointsEarned, timeSpentSeconds, isPerfect } = data;

    const totalExercises = await prisma.exercise.count({
      where: {
        moduleId,
        isPublished: true,
      },
    });

    // FIX: Validación temprana para división por cero
    if (totalExercises === 0) {
      logger.warn({ moduleId }, 'Módulo tiene 0 ejercicios, no se puede calcular porcentaje');
    }

    if (isCorrect) {
      // Check if user already completed this exercise correctly before
      const previousCorrectAttempts = await prisma.exerciseAttempt.count({
        where: {
          userId,
          exerciseId: data.exerciseId,
          status: 'CORRECT',
          // Exclude current attempt (check by comparing ids would need the attempt id)
        },
      });

      // Only increment exercisesCompleted if this is the first correct attempt for this exercise
      const shouldIncrementCompleted = previousCorrectAttempts === 0;

      const existingProgress = await prisma.progress.findUnique({
        where: {
          userId_moduleId: {
            userId,
            moduleId,
          },
        },
      });

      if (existingProgress) {
        const newExercisesCompleted = shouldIncrementCompleted
          ? existingProgress.exercisesCompleted + 1
          : existingProgress.exercisesCompleted;
        const newPoints = existingProgress.points + pointsEarned;
        // FIX: División por cero
        const newPercentage = totalExercises > 0 
          ? (newExercisesCompleted / totalExercises) * 100 
          : 0;
        const isCompleted = totalExercises > 0 && newExercisesCompleted >= totalExercises;

        await prisma.progress.update({
          where: { id: existingProgress.id },
          data: {
            exercisesCompleted: newExercisesCompleted,
            points: newPoints,
            percentage: newPercentage,
            isCompleted,
            completedAt: isCompleted ? new Date() : existingProgress.completedAt,
            perfectExercises: existingProgress.perfectExercises + (isPerfect ? 1 : 0),
            attemptsCount: existingProgress.attemptsCount + 1,
            totalTimeSpent: existingProgress.totalTimeSpent + timeSpentSeconds,
            totalExercises,
            lastAccessedAt: new Date(),
          },
        });
      } else {
        // FIX: División por cero
        const newPercentage = totalExercises > 0 
          ? (1 / totalExercises) * 100 
          : 0;
        await prisma.progress.create({
          data: {
            userId,
            moduleId,
            exercisesCompleted: 1,
            totalExercises,
            points: pointsEarned,
            percentage: newPercentage,
            isStarted: true,
            isCompleted: totalExercises === 1,
            startedAt: new Date(),
            completedAt: totalExercises === 1 ? new Date() : null,
            perfectExercises: isPerfect ? 1 : 0,
            attemptsCount: 1,
            totalTimeSpent: timeSpentSeconds,
            lastAccessedAt: new Date(),
          },
        });
      }
    } else {
      // Just update last accessed time and attempt count
      await prisma.progress.upsert({
        where: {
          userId_moduleId: {
            userId,
            moduleId,
          },
        },
        create: {
          userId,
          moduleId,
          totalExercises,
          isStarted: true,
          lastAccessedAt: new Date(),
          startedAt: new Date(),
          attemptsCount: 1,
          totalTimeSpent: timeSpentSeconds,
        },
        update: {
          lastAccessedAt: new Date(),
          attemptsCount: {
            increment: 1,
          },
          totalTimeSpent: {
            increment: timeSpentSeconds,
          },
        },
      });
    }

    logger.info({
      userId,
      moduleId,
      isCorrect,
      pointsEarned,
    }, 'Progress updated from attempt');
  }

  /**
   * Get overall progress for a user
   */
  async getUserOverallProgress(userId: string): Promise<OverallProgress> {
    const [progressRecords, _allModules, userRankings] = await Promise.all([
      prisma.progress.findMany({
        where: { userId },
        include: {
          modules: {
            select: {
              id: true,
              name: true,
              type: true,
              difficultyLevel: true,
            },
          },
        },
      }),
      prisma.modules.findMany({
        where: { isPublished: true },
        select: {
          id: true,
          name: true,
          type: true,
          difficultyLevel: true,
        },
      }),
      prisma.ranking.findMany({
        where: { userId },
      }),
    ]);

    // Calculate totals
    const totalPoints = progressRecords.reduce((sum: number, p) => sum + p.points, 0);
    const totalExercisesCompleted = progressRecords.reduce(
      (sum: number, p) => sum + p.exercisesCompleted,
      0
    );
    const totalModulesStarted = progressRecords.filter((p) => p.isStarted).length;
    const totalModulesCompleted = progressRecords.filter((p) => p.isCompleted).length;
    const perfectExercises = progressRecords.reduce(
      (sum: number, p) => sum + p.perfectExercises,
      0
    );
    const totalAttempts = progressRecords.reduce(
      (sum: number, p) => sum + p.attemptsCount,
      0
    );
    const totalTimeSpent = progressRecords.reduce(
      (sum: number, p) => sum + p.totalTimeSpent,
      0
    );

    // Calculate average score
    const validScores = progressRecords
      .map((p) => p.averageScore)
      .filter((score): score is number => score !== null);
    const averageScore =
      validScores.length > 0
        ? validScores.reduce((sum: number, score) => sum + score, 0) / validScores.length
        : 0;

    // Calculate current streak
    const currentStreak = await this.calculateCurrentStreak(userId);

    // Build module progress list
    const moduleProgressList: ModuleProgress[] = progressRecords.map((p) => {
      const base: ModuleProgress = {
        moduleId: p.moduleId,
        moduleName: p.modules.name,
        isStarted: p.isStarted,
        isCompleted: p.isCompleted,
        metrics: {
          exercisesCompleted: p.exercisesCompleted,
          totalExercises: p.totalExercises,
          points: p.points,
          percentage: p.percentage,
          totalTimeSpent: p.totalTimeSpent,
          perfectExercises: p.perfectExercises,
          attemptsCount: p.attemptsCount,
        },
      };
      if (p.startedAt) base.startedAt = p.startedAt;
      if (p.completedAt) base.completedAt = p.completedAt;
      if (p.lastAccessedAt) base.lastAccessedAt = p.lastAccessedAt;
      if (p.averageScore !== null) base.metrics.averageScore = p.averageScore;
      return base;
    });

    // Get global ranking
    const globalRanking = userRankings.find((r) => !r.moduleId);
    let ranking: { global: RankingEntry } | undefined;
    if (globalRanking) {
      const globalEntry: RankingEntry = {
        position: globalRanking.position,
        points: globalRanking.points,
        exercisesCompleted: globalRanking.exercisesCompleted,
        streak: globalRanking.streak,
        perfectExercises: globalRanking.perfectExercises,
        achievementsUnlocked: globalRanking.achievementsUnlocked,
      };
      if (globalRanking.averageScore !== null) {
        globalEntry.averageScore = globalRanking.averageScore;
      }
      ranking = { global: globalEntry };
    }

    logger.info({
      userId,
      totalPoints,
      totalModulesCompleted,
    }, 'Overall progress retrieved');

    const result: OverallProgress = {
      totalPoints,
      totalExercisesCompleted,
      totalModulesCompleted,
      totalModulesStarted,
      averageScore,
      totalTimeSpent,
      perfectExercises,
      totalAttempts,
      currentStreak,
      modules: moduleProgressList,
    };
    
    if (ranking) {
      result.ranking = ranking;
    }
    
    return result;
  }

  /**
   * Get progress for a specific module
   */
  async getModuleProgress(
    userId: string,
    moduleId: string
  ): Promise<ModuleProgressDetail> {
    // Get current count of published exercises for accurate percentage calculation
    const currentTotalExercises = await prisma.exercise.count({
      where: {
        moduleId,
        isPublished: true,
      },
    });

    const [progress, module] = await Promise.all([
      prisma.progress.findUnique({
        where: {
          userId_moduleId: {
            userId,
            moduleId,
          },
        },
      }),
      prisma.modules.findUnique({
        where: { id: moduleId },
        select: {
          name: true,
          totalExercises: true,
        },
      }),
    ]);

    if (!module) {
      throw new NotFoundError('Module');
    }

    if (!progress) {
      // Return empty progress for not started module
      return {
        moduleId,
        moduleName: module.name,
        isStarted: false,
        isCompleted: false,
        metrics: {
          exercisesCompleted: 0,
          totalExercises: currentTotalExercises,
          points: 0,
          percentage: 0,
          totalTimeSpent: 0,
          perfectExercises: 0,
          attemptsCount: 0,
        },
        exercisesRemaining: currentTotalExercises,
        averagePointsPerExercise: 0,
        timeSpentHours: 0,
      };
    }

    // Recalculate percentage dynamically based on current exercise count
    // This handles cases where new exercises are published after progress was recorded
    const dynamicPercentage = currentTotalExercises > 0
      ? (progress.exercisesCompleted / currentTotalExercises) * 100
      : 0;

    // Update isCompleted status based on current count
    const isNowCompleted = progress.exercisesCompleted >= currentTotalExercises;

    const exercisesRemaining = Math.max(0, currentTotalExercises - progress.exercisesCompleted);
    const averagePointsPerExercise =
      progress.exercisesCompleted > 0
        ? progress.points / progress.exercisesCompleted
        : 0;
    const timeSpentHours = progress.totalTimeSpent / 3600;

    // Get next exercise - find the exercise with lowest order that user hasn't completed correctly
    // First, get all exercises in the module
    const allExercises = await prisma.exercise.findMany({
      where: {
        moduleId,
        isPublished: true,
      },
      orderBy: {
        order: 'asc',
      },
      select: {
        id: true,
        order: true,
        difficulty: true,
      },
    });

    // Get exercise IDs that user has completed correctly
    const completedExerciseIds = await prisma.exerciseAttempt.findMany({
      where: {
        userId,
        exercises: {
          moduleId,
        },
        status: 'CORRECT',
      },
      select: {
        exerciseId: true,
      },
    });

    const completedIdsSet = new Set(completedExerciseIds.map(a => a.exerciseId));

    // Find the first exercise (by order) that user hasn't completed correctly
    const nextExercise = allExercises.find(ex => !completedIdsSet.has(ex.id)) || null;

    // Get last attempt
    const lastAttempt = await prisma.exerciseAttempt.findFirst({
      where: {
        userId,
        exercises: {
          moduleId,
        },
      },
      orderBy: {
        createdAt: 'desc',
      },
      select: {
        exerciseId: true,
        status: true,
        createdAt: true,
      },
    });

    logger.info({
      userId,
      moduleId,
      percentage: dynamicPercentage,
      storedPercentage: progress.percentage,
    }, 'Module progress retrieved');

    const result: ModuleProgressDetail = {
      moduleId: progress.moduleId,
      moduleName: module.name,
      isStarted: progress.isStarted,
      isCompleted: isNowCompleted,
      metrics: {
        exercisesCompleted: progress.exercisesCompleted,
        totalExercises: currentTotalExercises,
        points: progress.points,
        percentage: dynamicPercentage,
        totalTimeSpent: progress.totalTimeSpent,
        perfectExercises: progress.perfectExercises,
        attemptsCount: progress.attemptsCount,
      },
      exercisesRemaining,
      averagePointsPerExercise,
      timeSpentHours,
    };
    
    if (progress.startedAt) result.startedAt = progress.startedAt;
    if (isNowCompleted && progress.completedAt) result.completedAt = progress.completedAt;
    if (progress.lastAccessedAt) result.lastAccessedAt = progress.lastAccessedAt;
    if (progress.averageScore !== null) result.metrics.averageScore = progress.averageScore;
    if (nextExercise) result.nextExercise = nextExercise;
    if (lastAttempt) result.lastAttempt = lastAttempt;
    
    return result;
  }

  /**
   * Get progress by module type
   */
  async getProgressByModuleType(
    userId: string,
    type: ModuleType
  ): Promise<ModuleProgress[]> {
    const progressRecords = await prisma.progress.findMany({
      where: {
        userId,
        modules: {
          type,
        },
      },
      include: {
        modules: {
          select: {
            id: true,
            name: true,
            type: true,
            difficultyLevel: true,
          },
        },
      },
      orderBy: {
        modules: {
          order: 'asc',
        },
      },
    });

    return progressRecords.map((p) => {
      const base: ModuleProgress = {
        moduleId: p.moduleId,
        moduleName: p.modules.name,
        isStarted: p.isStarted,
        isCompleted: p.isCompleted,
        metrics: {
          exercisesCompleted: p.exercisesCompleted,
          totalExercises: p.totalExercises,
          points: p.points,
          percentage: p.percentage,
          totalTimeSpent: p.totalTimeSpent,
          perfectExercises: p.perfectExercises,
          attemptsCount: p.attemptsCount,
        },
      };
      if (p.startedAt) base.startedAt = p.startedAt;
      if (p.completedAt) base.completedAt = p.completedAt;
      if (p.lastAccessedAt) base.lastAccessedAt = p.lastAccessedAt;
      if (p.averageScore !== null) base.metrics.averageScore = p.averageScore;
      return base;
    });
  }

  /**
   * Get recent activity for a user
   */
  async getRecentActivity(
    userId: string,
    limit: number = 10
  ): Promise<
    Array<{
      type: 'exercise' | 'module';
      exerciseId?: string;
      moduleId: string;
      moduleName: string;
      status: string;
      pointsEarned: number;
      createdAt: Date;
    }>
  > {
    const recentAttempts = await prisma.exerciseAttempt.findMany({
      where: { userId },
      include: {
        exercises: {
          include: {
            modules: {
              select: {
                name: true,
              },
            },
          },
        },
      },
      orderBy: {
        createdAt: 'desc',
      },
      take: limit,
    });

    return recentAttempts.map(attempt => ({
      type: 'exercise',
      exerciseId: attempt.exerciseId,
      moduleId: attempt.exercises.moduleId,
      moduleName: attempt.exercises.modules.name,
      status: attempt.status,
      pointsEarned: attempt.pointsEarned,
      createdAt: attempt.createdAt,
    }));
  }

  /**
   * Get statistics summary
   */
  async getStatisticsSummary(
    userId: string
  ): Promise<{
    today: {
      exercisesCompleted: number;
      pointsEarned: number;
      timeSpent: number;
    };
    week: {
      exercisesCompleted: number;
      pointsEarned: number;
      timeSpent: number;
    };
    month: {
      exercisesCompleted: number;
      pointsEarned: number;
      timeSpent: number;
    };
    allTime: {
      exercisesCompleted: number;
      pointsEarned: number;
      timeSpent: number;
      modulesCompleted: number;
    };
  }> {
    const now = new Date();
    const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const weekStart = new Date(todayStart);
    weekStart.setDate(weekStart.getDate() - 7);
    const monthStart = new Date(todayStart);
    monthStart.setDate(monthStart.getDate() - 30);

    const [todayStats, weekStats, monthStats, allTimeStats] = await Promise.all([
      this.getStatsForPeriod(userId, todayStart, now),
      this.getStatsForPeriod(userId, weekStart, now),
      this.getStatsForPeriod(userId, monthStart, now),
      this.getAllTimeStats(userId),
    ]);

    return {
      today: todayStats,
      week: weekStats,
      month: monthStats,
      allTime: allTimeStats,
    };
  }

  /**
   * Get leaderboards
   */
  async getLeaderboards(options: {
    moduleId?: string;
    limit?: number;
    includeSelf?: boolean;
    userId?: string;
  } = {}): Promise<{
    leaderboard: Array<{
      position: number;
      userId: string;
      username: string;
      points: number;
      exercisesCompleted: number;
      streak: number;
    }>;
    userPosition?: {
      position: number;
      points: number;
    };
  }> {
    const { moduleId, limit = 50, includeSelf = false, userId } = options;

    const where: any = {};
    if (moduleId !== undefined) {
      where.moduleId = moduleId;
    } else {
      where.moduleId = null;
    }

    const leaderboard = await prisma.ranking.findMany({
      where,
      orderBy: [
        { points: 'desc' },
        { exercisesCompleted: 'desc' },
        { position: 'asc' },
      ],
      take: limit,
      include: {
        user: {
          select: {
            id: true,
            username: true,
          },
        },
      },
    });

    const result: {
      leaderboard: Array<{
        position: number;
        userId: string;
        username: string;
        points: number;
        exercisesCompleted: number;
        streak: number;
      }>;
      userPosition?: {
        position: number;
        points: number;
      };
    } = {
      leaderboard: leaderboard.map(entry => ({
        position: entry.position,
        userId: entry.user.id,
        username: entry.user.username,
        points: entry.points,
        exercisesCompleted: entry.exercisesCompleted,
        streak: entry.streak,
      })),
    };

    // Include user's position if requested
    if (includeSelf && userId) {
      const userRanking = await prisma.ranking.findFirst({
        where: {
          userId,
          ...where,
        },
      });

      if (userRanking) {
        result.userPosition = {
          position: userRanking.position,
          points: userRanking.points,
        };
      }
    }

    return result;
  }

  /**
   * Reset module progress (admin function)
   */
  async resetModuleProgress(
    userId: string,
    moduleId: string
  ): Promise<void> {
    await prisma.$transaction(async (tx) => {
      // Delete progress record
      await tx.progress.delete({
        where: {
          userId_moduleId: {
            userId,
            moduleId,
          },
        },
      });

      // Delete all attempts for exercises in this module
      const exerciseIds = await tx.exercise
        .findMany({
          where: { moduleId },
          select: { id: true },
        })
        .then(exercises => exercises.map(e => e.id));

      if (exerciseIds.length > 0) {
        await tx.exerciseAttempt.deleteMany({
          where: {
            userId,
            exerciseId: { in: exerciseIds },
          },
        });
      }
    });

    logger.info({
      userId,
      moduleId,
    }, 'Module progress reset');
  }

  /**
   * Calculate user's current streak
   */
  private async calculateCurrentStreak(userId: string): Promise<number> {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    const recentAttempts = await prisma.exerciseAttempt.findMany({
      where: {
        userId,
        status: 'CORRECT',
      },
      orderBy: {
        createdAt: 'desc',
      },
      select: {
        createdAt: true,
      },
      take: 365, // Check up to a year back
    });

    if (recentAttempts.length === 0) {
      return 0;
    }

    let streak = 0;
    let currentDate = today;

    // Get unique dates
    const uniqueDates = new Set(
      recentAttempts.map(a => {
        const d = new Date(a.createdAt);
        d.setHours(0, 0, 0, 0);
        return d.getTime();
      })
    );

    // Count consecutive days going backwards from today
    for (let i = 0; i < 365; i++) {
      const checkDate = new Date(currentDate);
      checkDate.setDate(checkDate.getDate() - i);

      if (uniqueDates.has(checkDate.getTime())) {
        streak++;
      } else if (i > 0) {
        // Allow skipping today if no activity yet
        break;
      }
    }

    return streak;
  }

  /**
   * Get statistics for a time period
   */
  private async getStatsForPeriod(
    userId: string,
    startDate: Date,
    endDate: Date
  ): Promise<{
    exercisesCompleted: number;
    pointsEarned: number;
    timeSpent: number;
  }> {
    const attempts = await prisma.exerciseAttempt.findMany({
      where: {
        userId,
        createdAt: {
          gte: startDate,
          lte: endDate,
        },
      },
    });

    const correctAttempts = attempts.filter(a => a.status === 'CORRECT');

    return {
      exercisesCompleted: correctAttempts.length,
      pointsEarned: attempts.reduce((sum, a) => sum + a.pointsEarned, 0),
      timeSpent: attempts.reduce((sum, a) => sum + a.timeSpentSeconds, 0),
    };
  }

  /**
   * Get all-time statistics
   */
  private async getAllTimeStats(userId: string): Promise<{
    exercisesCompleted: number;
    pointsEarned: number;
    timeSpent: number;
    modulesCompleted: number;
  }> {
    const [attempts, progress] = await Promise.all([
      prisma.exerciseAttempt.findMany({
        where: { userId },
      }),
      prisma.progress.findMany({
        where: { userId },
      }),
    ]);

    const correctAttempts = attempts.filter(a => a.status === 'CORRECT');

    return {
      exercisesCompleted: correctAttempts.length,
      pointsEarned: attempts.reduce((sum, a) => sum + a.pointsEarned, 0),
      timeSpent: attempts.reduce((sum, a) => sum + a.timeSpentSeconds, 0),
      modulesCompleted: progress.filter(p => p.isCompleted).length,
    };
  }
}

// ============================================
// EXPORT
// ============================================

export const progressService = new ProgressService();
export default progressService;
