/**
 * Exercise Service
 *
 * Business logic for exercise operations including
 * listing, retrieving, answer validation, and progress tracking
 */

import { prisma } from '../../shared/database/prisma.client';
import { NotFoundError, AuthorizationError } from '../../shared/types';
import { AttemptStatus } from '@prisma/client';
import { logger } from '../../shared/utils/logger';
import { ScoreCalculator } from '../ranking/calculators/score.calculator';
import { RankingService } from '../ranking/ranking.service';
import type {
  Exercise,
  ExerciseType,
  ExerciseDifficulty,
} from '@prisma/client';
import type {
  ExerciseAttemptInput,
  ExerciseAttemptResponse,
  SolutionStep,
  ExerciseHint,
} from '../../shared/types';

// ============================================
// RETRY HELPER para transacciones serializables
// ============================================

interface PrismaError {
  code?: string;
  message?: string;
}

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 50
): Promise<T> {
  let lastError: Error | null = null;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error as Error;
      
      // Check if it's a retryable error (deadlock or write conflict)
      const prismaError = error as PrismaError;
      const isRetryable = 
        prismaError.code === 'P2034' || // Transaction conflict
        prismaError.message?.includes('deadlock') ||
        prismaError.message?.includes('write conflict');
      
      if (!isRetryable || attempt === maxRetries - 1) {
        throw error;
      }
      
      // Exponential backoff with jitter
      const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 50;
      logger.warn({ attempt, delay, error: prismaError.message }, 'Transaction conflict, retrying...');
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

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

export interface ExerciseListItem {
  id: string;
  moduleId: string;
  topicId: string | null;
  type: ExerciseType;
  difficulty: ExerciseDifficulty;
  order: number;
  statement: string;
  points: number;
  timeLimitSeconds: number | null;
  isAIGenerated: boolean;
}

export interface ExerciseForAttempt {
  id: string;
  moduleId: string;
  type: ExerciseType;
  correctAnswer: string;
  solutionSteps: any;
  points: number;
  timeLimitSeconds: number | null;
  hints: any;
  multipleChoiceOptions: any;
}

export interface PracticeExerciseItem {
  id: string;
  type: ExerciseType;
  difficulty: ExerciseDifficulty;
  order: number;
  statement: string;
  points: number;
  timeLimitSeconds: number | null;
  userCompleted?: boolean;
}

export interface ExerciseListFilters {
  moduleId?: string;
  topicId?: string;
  type?: ExerciseType;
  difficulty?: ExerciseDifficulty;
  isPublished?: boolean;
  skip?: string[];
}

export interface ExerciseListOptions {
  skip?: number;
  take?: number;
  orderBy?: 'order' | 'difficulty' | 'points' | 'createdAt';
  orderDirection?: 'asc' | 'desc';
}

export interface ExerciseDetailOptions {
  includeModule?: boolean;
  includeTopic?: boolean;
  includeUserAttempts?: boolean;
  userId?: string | undefined;
  hideSolution?: boolean;
}

export interface ExerciseWithMetadata extends Exercise {
  moduleName?: string;
  topicName?: string;
  userAttempts?: number;
  userBestScore?: number;
  userCompleted?: boolean;
}

// Normalizes answer for comparison (handles whitespace, case for non-math answers)
function normalizeAnswer(answer: string): string {
  // Detect if answer contains LaTeX expressions
  const hasLatex = answer.includes('\\') || answer.includes('$') || answer.includes('{');

  let normalized = answer
    .trim()
    .replace(/\s+/g, ' ')
    .replace(/\\left\(/g, '(')
    .replace(/\\right\)/g, ')')
    .replace(/\\left\[/g, '[')
    .replace(/\\right\]/g, ']')
    .replace(/\$\$/g, '')
    .replace(/\$/g, '');

  // Only apply toLowerCase for non-LaTeX answers to preserve case-sensitive symbols
  if (!hasLatex) {
    normalized = normalized.toLowerCase();
  }

  return normalized;
}

// Compare two answers considering mathematical equivalence
function compareAnswers(userAnswer: string, correctAnswer: string): {
  isCorrect: boolean;
  isPartial?: boolean;
  confidence: number;
} {
  const normalizedUser = normalizeAnswer(userAnswer);
  const normalizedCorrect = normalizeAnswer(correctAnswer);

  // Exact match after normalization
  if (normalizedUser === normalizedCorrect) {
    return { isCorrect: true, confidence: 1.0 };
  }

  // Remove all whitespace for math expressions
  const noSpaceUser = normalizedUser.replace(/\s/g, '');
  const noSpaceCorrect = normalizedCorrect.replace(/\s/g, '');

  if (noSpaceUser === noSpaceCorrect) {
    return { isCorrect: true, confidence: 0.95 };
  }

  // Check if answer contains key components (for partial credit)
  const userWords = new Set(normalizedUser.split(/[\s,;()]+/));
  const correctWords = new Set(normalizedCorrect.split(/[\s,;()]+/));

  let matchCount = 0;
  for (const word of correctWords) {
    if (word.length > 2 && userWords.has(word)) {
      matchCount++;
    }
  }

  const significantWords = Array.from(correctWords).filter(w => w.length > 2);
  const matchRatio = significantWords.length > 0
    ? matchCount / significantWords.length
    : 0;

  if (matchRatio >= 0.7) {
    return { isCorrect: false, isPartial: true, confidence: matchRatio };
  }

  return { isCorrect: false, confidence: matchRatio };
}


// Generate feedback based on attempt result
function generateFeedback(
  isCorrect: boolean,
  isPartial: boolean,
  exercise: ExerciseForAttempt,
  attemptNumber: number
): string {
  if (isCorrect) {
    if (attemptNumber === 1) {
      return '¡Excelente! Respuesta correcta en el primer intento.';
    } else {
      return `¡Correcto! Lo lograste en ${attemptNumber} ${attemptNumber === 2 ? 'intentos' : 'intentos'}.`;
    }
  }

  if (isPartial) {
    return 'Tu respuesta está parcialmente correcta. Revisa los detalles y vuelve a intentarlo.';
  }

  const hints = (exercise.hints as unknown as ExerciseHint[]) || [];
  if (hints.length > 0) {
    return 'Respuesta incorrecta. Puedes usar una pista para ayudarte.';
  }

  return 'Respuesta incorrecta. Revisa el material y vuelve a intentarlo.';
}

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

class ExerciseService {
  /**
   * List exercises with optional filtering
   */
  async listExercises(
    filters: ExerciseListFilters = {},
    options: ExerciseListOptions = {}
  ): Promise<{ exercises: ExerciseListItem[]; total: number }> {
    const {
      moduleId,
      topicId,
      type,
      difficulty,
      isPublished = true,
      skip: skipIds = [],
    } = filters;
    const { skip = 0, take = 50, orderBy = 'order', orderDirection = 'asc' } = options;

    const where: any = {
      isPublished,
    };

    if (moduleId !== undefined) {
      where.moduleId = moduleId;
    }

    if (topicId !== undefined) {
      where.topicId = topicId;
    }

    if (type !== undefined) {
      where.type = type;
    }

    if (difficulty !== undefined) {
      where.difficulty = difficulty;
    }

    if (skipIds.length > 0) {
      where.id = { notIn: skipIds };
    }

    const [exercises, total] = await Promise.all([
      prisma.exercise.findMany({
        where,
        skip,
        take,
        orderBy: { [orderBy]: orderDirection },
        select: {
          id: true,
          moduleId: true,
          topicId: true,
          type: true,
          difficulty: true,
          order: true,
          statement: true,
          points: true,
          timeLimitSeconds: true,
          isAIGenerated: true,
        },
      }),
      prisma.exercise.count({ where }),
    ]);

    logger.info({
      count: exercises.length,
      total,
      filters,
    }, 'Exercises listed');

    return { exercises, total };
  }

  /**
   * Get exercise by ID with detailed information
   */
  async getExerciseById(
    id: string,
    options: ExerciseDetailOptions = {}
  ): Promise<ExerciseWithMetadata> {
    const {
      includeModule = false,
      includeTopic = false,
      includeUserAttempts = false,
      userId,
      hideSolution = true,
    } = options;

    const exercise = await prisma.exercise.findUnique({
      where: { id },
      include: {
        modules: includeModule,
        topics: includeTopic,
        exercise_attempts: includeUserAttempts && userId
          ? {
              where: { userId },
              select: {
                id: true,
                status: true,
                pointsEarned: true,
                isPerfect: true,
                createdAt: true,
              },
              orderBy: { createdAt: 'desc' },
            }
          : false,
      },
    });

    if (!exercise) {
      throw new NotFoundError('Exercise');
    }

    const result: ExerciseWithMetadata = {
      ...exercise,
      correctAnswer: hideSolution ? '' : exercise.correctAnswer,
      solutionSteps: hideSolution ? null : exercise.solutionSteps,
    };

    if (includeModule && exercise.modules) {
      (result as any).moduleName = exercise.modules.name;
    }

    if (includeTopic && exercise.topics) {
      (result as any).topicName = exercise.topics.name;
    }

    if (includeUserAttempts && userId && exercise.exercise_attempts) {
      (result as any).userAttempts = exercise.exercise_attempts.length;
      const bestAttempt = exercise.exercise_attempts.sort((a, b) => b.pointsEarned - a.pointsEarned)[0];
      (result as any).userBestScore = bestAttempt?.pointsEarned || 0;
      (result as any).userCompleted = exercise.exercise_attempts.some(a => a.status === 'CORRECT');
    }

    logger.info({ exerciseId: id }, 'Exercise retrieved');

    return result;
  }

  /**
   * Submit an exercise attempt
   */
  async submitAttempt(
    exerciseId: string,
    userId: string,
    attemptData: ExerciseAttemptInput
  ): Promise<ExerciseAttemptResponse> {
    const { answer, timeSpent, hintsUsed = 0, skipped = false } = attemptData;

    // FIX: Mover TODO dentro de la transacción para evitar race condition en attemptNumber
    // FIX: Usar withRetry con más intentos para manejar deadlocks de transacciones serializables
    const result = await withRetry(async () => {
      return await prisma.$transaction(async (tx) => {
        // Get exercise details
        const exercise = await tx.exercise.findUnique({
          where: { id: exerciseId },
          select: {
            id: true,
            moduleId: true,
            correctAnswer: true,
            solutionSteps: true,
            points: true,
            timeLimitSeconds: true,
            type: true,
            hints: true,
            multipleChoiceOptions: true,
          },
        });

        if (!exercise) {
          throw new NotFoundError('Exercise');
        }

        // FIX: Contar intentos DENTRO de la transacción para evitar race condition
        const previousAttempts = await tx.exerciseAttempt.count({
          where: {
            userId,
            exerciseId,
          },
        });

        const attemptNumber = previousAttempts + 1;

        let status: AttemptStatus;
        let isCorrect = false;
        let isPartial = false;
        let pointsEarned = 0;
        let message: string;

        if (skipped) {
          status = 'PENDING';
          isCorrect = false;
          message = 'Ejercicio omitido. Puedes volver a intentarlo más tarde.';
        } else {
          // Compare answers
          const comparison = compareAnswers(answer, exercise.correctAnswer);
          isCorrect = comparison.isCorrect;
          isPartial = comparison.isPartial || false;

          if (isCorrect) {
            status = 'CORRECT';
            // Use ScoreCalculator for unified point calculation (L-01)
            const scoreResult = await ScoreCalculator.calculate({
              exerciseId,
              userId,
              isCorrect: true,
              timeSpentSeconds: timeSpent,
              hintsUsed,
              attemptNumber,
            });
            pointsEarned = scoreResult.finalPoints;
          } else if (isPartial) {
            status = 'PARTIAL';
            pointsEarned = Math.floor(exercise.points * 0.3);
          } else {
            status = 'INCORRECT';
            pointsEarned = 0;
          }

          message = generateFeedback(isCorrect, isPartial, exercise, attemptNumber);
        }

        // Create attempt record
        const newAttempt = await tx.exerciseAttempt.create({
          data: {
            userId,
            exerciseId,
            userAnswer: answer,
            status,
            pointsEarned,
            timeSpentSeconds: timeSpent,
            hintsUsed,
            feedback: message,
            attemptNumber,
            isPerfect: isCorrect && hintsUsed === 0 && attemptNumber === 1,
            skipped,
          },
        });

        // Update or create progress record
        const totalExercises = await tx.exercise.count({
          where: {
            moduleId: exercise.moduleId,
            isPublished: true,
          },
        });

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

        const progressData = {
          totalExercises,
          lastAccessedAt: new Date(),
        };

        if (status === 'CORRECT') {
          // Check if user already had a CORRECT attempt for this exercise
          // FIX: Excluir el attempt recién creado para evitar race condition
          const previousCorrectAttempt = await tx.exerciseAttempt.findFirst({
            where: {
              userId,
              exerciseId,
              status: 'CORRECT',
              id: { not: newAttempt.id }, // Excluir el recién creado
              createdAt: { lt: newAttempt.createdAt }, // Solo intentos anteriores
            },
            select: { id: true },
          });

          // isFirstCorrect is true if this is the first time the user correctly solved this exercise
          const isFirstCorrect = !previousCorrectAttempt;

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

          if (existingProgress) {
            // Only increment exercisesCompleted if this is the first correct attempt for this exercise
            const newExercisesCompleted = isFirstCorrect
              ? 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 tx.progress.update({
              where: { id: existingProgress.id },
              data: {
                exercisesCompleted: newExercisesCompleted,
                points: newPoints,
                percentage: newPercentage,
                isCompleted,
                completedAt: isCompleted ? new Date() : existingProgress.completedAt,
                perfectExercises: existingProgress.perfectExercises + (newAttempt.isPerfect ? 1 : 0),
                attemptsCount: existingProgress.attemptsCount + 1,
                totalTimeSpent: existingProgress.totalTimeSpent + timeSpent,
                ...progressData,
              },
            });
          } else {
            // First ever correct attempt for this module
            // FIX: División por cero
            const newPercentage = totalExercises > 0 
              ? (1 / totalExercises) * 100 
              : 0;
            await tx.progress.create({
              data: {
                userId,
                moduleId: exercise.moduleId,
                exercisesCompleted: 1,
                points: pointsEarned,
                percentage: newPercentage,
                isStarted: true,
                isCompleted: totalExercises === 1,
                startedAt: new Date(),
                completedAt: totalExercises === 1 ? new Date() : null,
                perfectExercises: newAttempt.isPerfect ? 1 : 0,
                attemptsCount: 1,
                totalTimeSpent: timeSpent,
                ...progressData,
              },
            });
          }
        } else {
          // Just update last accessed time
          await tx.progress.upsert({
            where: {
              userId_moduleId: {
                userId,
                moduleId: exercise.moduleId,
              },
            },
            create: {
              userId,
              moduleId: exercise.moduleId,
              totalExercises,
              isStarted: status !== 'PENDING',
              lastAccessedAt: new Date(),
              startedAt: status !== 'PENDING' ? new Date() : null,
            },
            update: {
              lastAccessedAt: new Date(),
              attemptsCount: {
                increment: 1,
              },
              totalTimeSpent: {
                increment: timeSpent,
              },
            },
          });
        }

        // Return all data needed for response
        return {
          newAttempt,
          exercise,
          isCorrect,
          isPartial,
          pointsEarned,
          message,
          attemptNumber,
        };
      }, {
        // FIX: Transacción serializable para máxima consistencia
        isolationLevel: 'Serializable',
        maxWait: 5000,
        timeout: 10000,
      });
    }, 5, 100); // 5 retries, 100ms base delay para alta concurrencia

    const { newAttempt, exercise, isCorrect, pointsEarned, message, attemptNumber } = result;

    logger.info({
      exerciseId,
      userId,
      status: newAttempt.status,
      pointsEarned,
      attemptNumber,
    }, 'Exercise attempt submitted');

    // Trigger ranking update in background (A-02)
    // This updates global and module rankings based on the submission
    RankingService.processExerciseSubmission(
      {
        exerciseId,
        userId,
        userAnswer: answer,
        timeSpentSeconds: timeSpent,
        hintsUsed,
        attemptNumber,
        pointsEarned, // Pass pre-calculated points to avoid double calculation (L-01)
      },
      isCorrect
    ).catch((error) => {
      // Log error but don't fail the request
      logger.error({ error, exerciseId, userId }, 'Failed to process ranking submission');
    });

    // Prepare response
    const response: ExerciseAttemptResponse = {
      isCorrect,
      points: pointsEarned,
      message,
      ...(skipped && { skipped }),
    };

    // Include correct answer and solution only if attempt was correct
    if (isCorrect) {
      response.correctAnswer = exercise.correctAnswer;
      response.solutionSteps = (exercise.solutionSteps as unknown as SolutionStep[]) || undefined;
    }

    return response;
  }

  /**
   * Get exercise solution (with validation that user has completed it)
   */
  async getExerciseSolution(
    exerciseId: string,
    userId?: string
  ): Promise<{
    correctAnswer: string;
    solutionSteps: SolutionStep[];
    hasCompleted: boolean;
  }> {
    const exercise = await prisma.exercise.findUnique({
      where: { id: exerciseId },
      select: {
        correctAnswer: true,
        solutionSteps: true,
        exercise_attempts: userId
          ? {
              where: {
                userId,
                status: 'CORRECT',
              },
              take: 1,
            }
          : false,
      },
    });

    if (!exercise) {
      throw new NotFoundError('Exercise');
    }

    const hasCompleted = userId && exercise.exercise_attempts && exercise.exercise_attempts.length > 0;

    if (!hasCompleted) {
      throw new AuthorizationError('Must complete exercise before viewing solution');
    }

    logger.info({
      exerciseId,
      userId,
      hasCompleted,
    }, 'Exercise solution retrieved');

    return {
      correctAnswer: exercise.correctAnswer,
      solutionSteps: (exercise.solutionSteps as unknown as SolutionStep[]) || [],
      hasCompleted: true,
    };
  }

  /**
   * Get exercise hints
   */
  async getExerciseHints(exerciseId: string): Promise<{
    hints: ExerciseHint[];
    totalHints: number;
  }> {
    const exercise = await prisma.exercise.findUnique({
      where: { id: exerciseId },
      select: {
        hints: true,
      },
    });

    if (!exercise) {
      throw new NotFoundError('Exercise');
    }

    const hints = (exercise.hints as unknown as ExerciseHint[]) || [];

    return {
      hints,
      totalHints: hints.length,
    };
  }

  /**
   * Get user's attempts for an exercise with pagination
   */
  async getUserAttempts(
    exerciseId: string,
    userId: string,
    options: { page?: number; limit?: number } = {}
  ): Promise<{
    attempts: Array<{
      id: string;
      status: AttemptStatus;
      pointsEarned: number;
      isPerfect: boolean;
      hintsUsed: number;
      timeSpentSeconds: number;
      feedback: string;
      createdAt: Date;
    }>;
    totalAttempts: number;
    bestScore: number;
    hasCompleted: boolean;
    pagination: {
      page: number;
      limit: number;
      total: number;
      totalPages: number;
      hasNext: boolean;
      hasPrev: boolean;
    };
  }> {
    const { page = 1, limit = 10 } = options;
    const skip = (page - 1) * limit;

    const exercise = await prisma.exercise.findUnique({
      where: { id: exerciseId },
      select: { id: true },
    });

    if (!exercise) {
      throw new NotFoundError('Exercise');
    }

    // Get total count separately (L-07)
    const totalAttempts = await prisma.exerciseAttempt.count({
      where: {
        exerciseId,
        userId,
      },
    });

    const attempts = await prisma.exerciseAttempt.findMany({
      where: {
        exerciseId,
        userId,
      },
      orderBy: {
        createdAt: 'desc',
      },
      skip,
      take: limit,
    });

    const mappedAttempts = attempts.map(a => ({
      ...a,
      feedback: a.feedback ?? '',
    }));

    // Get best score using aggregate (L-02: more efficient than loading all records)
    const bestScoreResult = await prisma.exerciseAttempt.aggregate({
      _max: { pointsEarned: true },
      where: {
        userId,
        exerciseId,
        status: 'CORRECT',
      },
    });

    const bestScore = bestScoreResult._max.pointsEarned || 0;
    const hasCompleted = bestScoreResult._max.pointsEarned !== null;

    const totalPages = Math.ceil(totalAttempts / limit);

    logger.info({
      exerciseId,
      userId,
      totalAttempts,
      returnedCount: attempts.length,
      page,
      limit,
      hasCompleted,
    }, 'User attempts retrieved');

    return {
      attempts: mappedAttempts,
      totalAttempts,
      bestScore,
      hasCompleted,
      pagination: {
        page,
        limit,
        total: totalAttempts,
        totalPages,
        hasNext: page < totalPages,
        hasPrev: page > 1,
      },
    };
  }

  /**
   * Get next exercise in module
   */
  async getNextExercise(currentExerciseId: string): Promise<Exercise | null> {
    const currentExercise = await prisma.exercise.findUnique({
      where: { id: currentExerciseId },
      select: {
        moduleId: true,
        order: true,
      },
    });

    if (!currentExercise) {
      throw new NotFoundError('Exercise');
    }

    const nextExercise = await prisma.exercise.findFirst({
      where: {
        moduleId: currentExercise.moduleId,
        order: {
          gt: currentExercise.order,
        },
        isPublished: true,
      },
      orderBy: {
        order: 'asc',
      },
    });

    if (!nextExercise) {
      return null;
    }

    logger.info({
      currentExerciseId,
      nextExerciseId: nextExercise.id,
    }, 'Next exercise retrieved');

    return nextExercise;
  }

  /**
   * Get exercises for a module (optimized for practice mode)
   */
  async getModuleExercisesForPractice(
    moduleId: string,
    userId: string,
    options: {
      difficulty?: ExerciseDifficulty;
      count?: number;
      excludeCompleted?: boolean;
    } = {}
  ): Promise<{
    exercises: Array<PracticeExerciseItem & { userCompleted?: boolean }>;
    totalInModule: number;
  }> {
    const { difficulty, count = 10, excludeCompleted = false } = options;

    const where: any = {
      moduleId,
      isPublished: true,
    };

    if (difficulty) {
      where.difficulty = difficulty;
    }

    if (excludeCompleted) {
      const completedExerciseIds = await prisma.exerciseAttempt
        .findMany({
          where: {
            userId,
            status: 'CORRECT',
          },
          select: {
            exerciseId: true,
          },
        })
        .then(attempts => [...new Set(attempts.map(a => a.exerciseId))]);

      if (completedExerciseIds.length > 0) {
        where.id = { notIn: completedExerciseIds };
      }
    }

    const [exercises, totalInModule] = await Promise.all([
      prisma.exercise.findMany({
        where,
        take: count,
        orderBy: {
          order: 'asc',
        },
        select: {
          id: true,
          type: true,
          difficulty: true,
          order: true,
          statement: true,
          points: true,
          timeLimitSeconds: true,
        },
      }),
      prisma.exercise.count({
        where: {
          moduleId,
          isPublished: true,
        },
      }),
    ]);

    // Mark completed exercises
    const completedIds = new Set(
      (
        await prisma.exerciseAttempt.findMany({
          where: {
            userId,
            exerciseId: { in: exercises.map(e => e.id) },
            status: 'CORRECT',
          },
          select: { exerciseId: true },
        })
      ).map(a => a.exerciseId)
    );

    const exercisesWithStatus = exercises.map(e => ({
      ...e,
      userCompleted: completedIds.has(e.id),
    }));

    return {
      exercises: exercisesWithStatus,
      totalInModule,
    };
  }
}

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

export const exerciseService = new ExerciseService();
export default exerciseService;
