/**
 * Ranking Service
 *
 * Main service that orchestrates ranking, scoring, and badge operations.
 * Provides methods for updating rankings, retrieving anonymous leaderboards,
 * and managing achievements.
 */

import { prisma } from '../../shared/database/prisma.client';
import { logger } from '../../shared/utils/logger';
import { ScoreCalculator } from './calculators/score.calculator';
import { PositionCalculator } from './calculators/position.calculator';
import { BadgeAwarder } from './calculators/badge.awarder';
import { GlobalRankingEntry, ModuleRankingEntry, UserRankingInfo } from './calculators/position.calculator';
import { UserBadgeProgress } from './calculators/badge.awarder';

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

export interface PeriodRankingEntry {
  rank: number;
  position: number;
  displayName: string;
  points: number;
  exercisesCompleted: number;
  streak: number;
  perfectExercises: number;
  achievementsUnlocked: number;
}

export interface RankingServiceOptions {
  limit?: number;
  offset?: number;
  aroundUser?: boolean;
  radius?: number;
}

export interface ExerciseSubmissionData {
  exerciseId: string;
  userId: string;
  userAnswer: string;
  timeSpentSeconds: number;
  hintsUsed: number;
  attemptNumber: number;
  pointsEarned?: number; // Optional: pre-calculated points to avoid double calculation (L-01)
}

export interface ExerciseSubmissionResult {
  isCorrect: boolean;
  pointsEarned: number;
  scoreBreakdown: string[];
  badgesAwarded: Array<{
    code: string;
    name: string;
    icon: string;
    rarity: string;
    points: number;
    message: string;
  }>;
  newPosition?: {
    global: number;
    module?: number;
  };
}

// ============================================
// RANKING SERVICE
// ============================================

export class RankingService {
  /**
   * Get global ranking (anonymous - no personal data)
   */
  static async getGlobalRanking(
    options: RankingServiceOptions = {}
  ): Promise<{ rankings: GlobalRankingEntry[]; total: number }> {
    const { limit = 100, offset = 0 } = options;

    logger.debug({ limit, offset }, 'Fetching global ranking');

    const rankings = await PositionCalculator.getGlobalRanking(limit, offset);
    const total = await prisma.ranking.count({
      where: { moduleId: null },
    });

    return { rankings, total };
  }

  /**
   * Get module ranking (anonymous)
   */
  static async getModuleRanking(
    moduleId: string,
    options: RankingServiceOptions = {}
  ): Promise<{ rankings: ModuleRankingEntry[]; total: number }> {
    const { limit = 100, offset = 0 } = options;

    // Verify module exists
    const module = await prisma.modules.findUnique({
      where: { id: moduleId },
      select: { id: true, name: true },
    });

    if (!module) {
      throw new Error(`Module ${moduleId} not found`);
    }

    logger.debug({ moduleId, limit, offset }, 'Fetching module ranking');

    const rankings = await PositionCalculator.getModuleRanking(moduleId, limit, offset);
    const total = await prisma.ranking.count({
      where: { moduleId },
    });

    return { rankings, total };
  }

  /**
   * Get user's ranking information (anonymous representation)
   */
  static async getUserRankingInfo(userId: string): Promise<UserRankingInfo> {
    logger.debug({ userId }, 'Fetching user ranking info');

    return await PositionCalculator.getUserRankingInfo(userId);
  }

  /**
   * Get ranking around user's position
   */
  static async getRankingAroundUser(
    userId: string,
    moduleId?: string,
    radius: number = 5
  ): Promise<GlobalRankingEntry[]> {
    logger.debug({ userId, moduleId, radius }, 'Fetching ranking around user');

    return await PositionCalculator.getRankingAroundUser(userId, moduleId || null, radius);
  }

  /**
   * Get all available badges
   */
  static async getAllBadges(): Promise<UserBadgeProgress[]> {
    // Return badge definitions without user-specific progress
    const { BADGE_DEFINITIONS } = await import('./definitions/badge-definitions');

    return BADGE_DEFINITIONS.map((badge) => ({
      code: badge.code,
      name: badge.name,
      description: badge.description,
      category: badge.category,
      rarity: badge.rarity,
      icon: badge.icon,
      requirementValue: badge.requirementValue,
      currentProgress: 0,
      unlocked: false,
    }));
  }

  /**
   * Get user's badges with progress
   */
  static async getUserBadges(userId: string): Promise<UserBadgeProgress[]> {
    logger.debug({ userId }, 'Fetching user badges');

    return await BadgeAwarder.getUserBadges(userId);
  }

  /**
   * Get user's unlocked badges only
   */
  static async getUnlockedBadges(userId: string): Promise<UserBadgeProgress[]> {
    logger.debug({ userId }, 'Fetching unlocked badges');

    return await BadgeAwarder.getUnlockedBadges(userId);
  }

  /**
   * Get user's badge progress for a specific badge
   */
  static async getBadgeProgress(userId: string, badgeCode: string): Promise<UserBadgeProgress | null> {
    logger.debug({ userId, badgeCode }, 'Fetching badge progress');

    return await BadgeAwarder.getBadgeProgress(userId, badgeCode);
  }

  /**
   * Process exercise submission and award points/badges
   * This is called after an exercise is validated
   */
  static async processExerciseSubmission(
    data: ExerciseSubmissionData,
    isCorrect: boolean
  ): Promise<ExerciseSubmissionResult> {
    const { exerciseId, userId, timeSpentSeconds, hintsUsed, attemptNumber, pointsEarned: preCalculatedPoints } = data;

    logger.info({
      userId,
      exerciseId,
      isCorrect,
      timeSpentSeconds,
      hintsUsed,
    }, 'Processing exercise submission');

    // 1. Calculate score only if not pre-calculated (L-01: avoid double calculation)
    let finalPointsEarned: number;
    let scoreBreakdown: string[];

    if (preCalculatedPoints !== undefined) {
      // Use pre-calculated points to avoid redundant calculation
      finalPointsEarned = preCalculatedPoints;
      scoreBreakdown = ['Points provided from previous calculation'];
    } else {
      // Calculate score when not provided
      const scoreResult = await ScoreCalculator.calculate({
        exerciseId,
        userId,
        isCorrect,
        timeSpentSeconds,
        hintsUsed,
        attemptNumber,
      });
      finalPointsEarned = scoreResult.finalPoints;
      scoreBreakdown = scoreResult.breakdown;
    }

    // 2. Get exercise info for module ranking
    const exercise = await prisma.exercise.findUnique({
      where: { id: exerciseId },
      select: { moduleId: true },
    });

    // 3. Update rankings
    const globalRanking = await PositionCalculator.updateUserGlobalRanking(userId);
    const moduleRanking = exercise?.moduleId
      ? await PositionCalculator.updateUserModuleRanking(userId, exercise.moduleId)
      : null;

    // 4. Check and award badges
    const badgeResult = await BadgeAwarder.checkAndAwardBadgesAfterExercise(
      userId,
      exerciseId,
      isCorrect
    );

    // 5. Prepare result
    const result: ExerciseSubmissionResult = {
      isCorrect,
      pointsEarned: finalPointsEarned,
      scoreBreakdown,
      badgesAwarded: badgeResult.badges,
      newPosition: {
        global: globalRanking.position,
        ...(moduleRanking?.position !== undefined && { module: moduleRanking.position }),
      },
    };

    logger.info({
      userId,
      pointsEarned: result.pointsEarned,
      badgesAwarded: result.badgesAwarded.length,
      newGlobalPosition: globalRanking.position,
    }, 'Exercise submission processed');

    return result;
  }

  /**
   * Recalculate all rankings (admin operation)
   */
  static async recalculateAllRankings(): Promise<void> {
    logger.info('Starting full ranking recalculation');

    await PositionCalculator.recalculateAllGlobalRankings();

    // Get all modules
    const modules = await prisma.modules.findMany({
      select: { id: true },
    });

    for (const module of modules) {
      await PositionCalculator.recalculateAllModuleRankings(module.id);
    }

    logger.info('Full ranking recalculation complete');
  }

  /**
   * Initialize all badges in the database
   */
  static async initializeBadges(): Promise<void> {
    logger.info('Initializing badges');

    await BadgeAwarder.initializeBadges();

    logger.info('Badges initialized successfully');
  }

  /**
   * Get ranking statistics
   */
  static async getRankingStatistics(): Promise<{
    totalUsers: number;
    totalPointsAwarded: number;
    totalExercisesCompleted: number;
    averageScore: number;
    topBadges: Array<{ code: string; name: string; unlockedCount: number }>;
  }> {
    const [
      totalUsers,
      totalPoints,
      totalExercises,
      topBadgesData,
    ] = await Promise.all([
      prisma.user.count({ where: { isActive: true } }),
      prisma.ranking.aggregate({
        where: { moduleId: null },
        _sum: { points: true },
      }),
      prisma.ranking.aggregate({
        where: { moduleId: null },
        _sum: { exercisesCompleted: true },
      }),
      prisma.userAchievement.groupBy({
        by: ['achievementId'],
        where: { unlockedAt: { not: null } },
        _count: { achievementId: true },
        orderBy: { _count: { achievementId: 'desc' } },
        take: 10,
      }),
    ]);

    // Calculate average score
    const averageScoreResult = await prisma.exerciseAttempt.aggregate({
      where: { status: 'CORRECT' },
      _avg: { pointsEarned: true },
    });

    // Get badge details
    const topBadges = await Promise.all(
      topBadgesData.map(async (b) => {
        const achievement = await prisma.achievement.findUnique({
          where: { id: b.achievementId },
          select: { code: true, name: true },
        });
        return {
          code: achievement?.code || b.achievementId,
          name: achievement?.name || 'Unknown',
          unlockedCount: b._count.achievementId,
        };
      })
    );

    return {
      totalUsers,
      totalPointsAwarded: totalPoints._sum.points || 0,
      totalExercisesCompleted: totalExercises._sum.exercisesCompleted || 0,
      averageScore: Math.round((averageScoreResult._avg.pointsEarned || 0) * 100) / 100,
      topBadges,
    };
  }

  /**
   * Get user's achievement summary
   */
  static async getUserAchievementSummary(userId: string): Promise<{
    totalBadges: number;
    unlockedBadges: number;
    totalPoints: number;
    currentStreak: number;
    longestStreak: number;
    globalPosition: number;
    perfectExercises: number;
    completionPercentage: number;
  }> {
    const [
      totalBadges,
      unlockedBadges,
      ranking,
      streakInfo,
      perfectExercises,
      completedModules,
      totalModules,
    ] = await Promise.all([
      prisma.achievement.count({ where: { isActive: true } }),
      prisma.userAchievement.count({
        where: { userId, unlockedAt: { not: null } },
      }),
      prisma.ranking.findFirst({
        where: {
          userId,
          moduleId: null,
        },
        select: {
          points: true,
          position: true,
          perfectExercises: true,
          streak: true,
          longestStreak: true,
        },
      }),
      ScoreCalculator.getUserStreak(userId),
      prisma.exerciseAttempt.count({
        where: { userId, isPerfect: true },
      }),
      prisma.progress.count({
        where: { userId, isCompleted: true },
      }),
      prisma.modules.count({ where: { isPublished: true } }),
    ]);

    // Use stored longestStreak from Ranking table (L-02)
    const longestStreak = ranking?.longestStreak || streakInfo.currentStreak;

    // Update longestStreak if current streak is higher (using findFirst + update/create)
    if (streakInfo.currentStreak > (ranking?.longestStreak || 0)) {
      const existingRanking = await prisma.ranking.findFirst({
        where: { userId, moduleId: null },
      });
      
      if (existingRanking) {
        await prisma.ranking.update({
          where: { id: existingRanking.id },
          data: { longestStreak: streakInfo.currentStreak },
        });
      } else {
        await prisma.ranking.create({
          data: {
            userId,
            moduleId: null,
            position: 0,
            points: 0,
            streak: 0,
            longestStreak: streakInfo.currentStreak,
          },
        });
      }
    }

    return {
      totalBadges,
      unlockedBadges,
      totalPoints: ranking?.points || 0,
      currentStreak: streakInfo.currentStreak,
      longestStreak,
      globalPosition: ranking?.position || 0,
      perfectExercises,
      completionPercentage: totalModules > 0
        ? Math.round((completedModules / totalModules) * 100)
        : 0,
    };
  }

  /**
   * Force update user ranking (for admin or testing)
   */
  static async updateUserRanking(userId: string): Promise<void> {
    logger.info({ userId }, 'Force updating user ranking');

    await PositionCalculator.updateUserGlobalRanking(userId);

    // Get all modules the user has progress in
    const moduleProgress = await prisma.progress.findMany({
      where: { userId },
      select: { moduleId: true },
    });

    for (const progress of moduleProgress) {
      await PositionCalculator.updateUserModuleRanking(userId, progress.moduleId);
    }

    logger.info({ userId }, 'User ranking updated');
  }

  /**
   * Clean up orphaned ranking entries
   */
  static async cleanupRankingEntries(): Promise<number> {
    logger.info('Cleaning up orphaned ranking entries');

    const result = await prisma.ranking.deleteMany({
      where: {
        user: {
          isActive: false,
        },
      },
    });

    logger.info({ deleted: result.count }, 'Orphaned ranking entries cleaned up');

    return result.count;
  }

  /**
   * Get leaderboard for a specific time period
   * Calculates points earned within the specified period from ExerciseAttempt
   * Returns anonymous ranking with displayName only (no userId exposed - C-04)
   */
  static async getPeriodRanking(
    period: 'daily' | 'weekly' | 'monthly' | 'all-time',
    options: RankingServiceOptions = {}
  ): Promise<{ rankings: PeriodRankingEntry[]; total: number; period: string }> {
    const { limit = 100, offset = 0 } = options;

    logger.debug({ period, limit, offset }, 'Fetching period ranking');

    // For all-time, use existing global ranking but add displayName
    if (period === 'all-time') {
      // Fetch rankings with userIds to get displayNames
      const rankingUsers = await prisma.ranking.findMany({
        where: { moduleId: null },
        orderBy: [
          { points: 'desc' },
          { exercisesCompleted: 'desc' },
          { lastUpdated: 'asc' },
        ],
        take: limit,
        skip: offset,
        select: {
          userId: true,
          position: true,
          points: true,
          exercisesCompleted: true,
          streak: true,
          perfectExercises: true,
          achievementsUnlocked: true,
        },
      });

      const total = await prisma.ranking.count({ where: { moduleId: null } });

      // Fetch usernames for these users
      const userIds = rankingUsers.map((r) => r.userId);
      const users = await prisma.user.findMany({
        where: { id: { in: userIds } },
        select: { id: true, username: true },
      });
      const userMap = new Map(users.map((u) => [u.id, u.username]));

      return {
        rankings: rankingUsers.map((r, idx) => ({
          rank: offset + idx + 1,
          position: r.position,
          displayName: userMap.get(r.userId) || `User${offset + idx + 1}`,
          points: r.points,
          exercisesCompleted: r.exercisesCompleted,
          streak: r.streak,
          perfectExercises: r.perfectExercises,
          achievementsUnlocked: r.achievementsUnlocked,
        })),
        total,
        period,
      };
    }

    // Calculate date filter based on period
    const now = new Date();
    let startDate: Date;

    switch (period) {
      case 'daily':
        startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); // Last 24 hours
        break;
      case 'weekly':
        startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // Last 7 days
        break;
      case 'monthly':
        startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // Last 30 days
        break;
      default:
        startDate = new Date(0); // All time fallback
    }

    // Aggregate points from ExerciseAttempt within the period
    // Only count correct attempts and use earnedAt for temporal filtering
    const userPoints = await prisma.exerciseAttempt.groupBy({
      by: ['userId'],
      where: {
        status: 'CORRECT',
        pointsEarned: { gt: 0 },
        earnedAt: { gte: startDate },
      },
      _sum: {
        pointsEarned: true,
      },
      _count: {
        id: true,
      },
    });

    // Sort by points (userId is internal only, not exposed in response)
    const sortedUsers = userPoints
      .map((u) => ({
        userId: u.userId,
        points: u._sum.pointsEarned || 0,
        exercisesCompleted: u._count.id,
      }))
      .sort((a, b) => b.points - a.points);

    const total = sortedUsers.length;

    // Apply pagination
    const paginatedUsers = sortedUsers.slice(offset, offset + limit);

    // Fetch usernames for paginated users only
    const userIds = paginatedUsers.map((u) => u.userId);
    const users = await prisma.user.findMany({
      where: { id: { in: userIds } },
      select: { id: true, username: true },
    });
    const userMap = new Map(users.map((u) => [u.id, u.username]));

    // Build response with displayName only (no userId exposed - C-04)
    const rankings: PeriodRankingEntry[] = paginatedUsers.map((user, idx) => ({
      rank: offset + idx + 1,
      position: offset + idx + 1,
      displayName: userMap.get(user.userId) || `User${offset + idx + 1}`,
      points: user.points,
      exercisesCompleted: user.exercisesCompleted,
      streak: 0, // Streak is not period-specific
      perfectExercises: 0, // Would need additional query
      achievementsUnlocked: 0, // Not period-specific
    }));

    return { rankings, total, period };
  }
}

export default RankingService;
