(function (global) {
  "use strict";

  /**
   * QuizManager: Clase principal para gestionar cuestionarios basados en un archivo Excel.
   */
  class QuizManager {
    /**
     * @constructor
     * @param {Object} config - Configuración del cuestionario.
     * @param {string} config.excelFileUrl - URL del archivo Excel que contiene las preguntas.
     * @param {Object} [config.mandatoryFields] - Campos obligatorios en el archivo Excel para el cuestionario.
     * @param {string} [config.mandatoryFields.question="pregunta"] - Nombre de la columna para el texto de la pregunta.
     * @param {string} [config.mandatoryFields.correctOption="opcion_c"] - Nombre de la columna que identifica la respuesta correcta.
     * @param {string} [config.mandatoryFields.correctFeedback="retroalimentacion_correcta"] - Nombre de la columna para la retroalimentación en respuestas correctas.
     * @param {string} [config.mandatoryFields.incorrectFeedback="retroalimentacion_incorrecta"] - Nombre de la columna para la retroalimentación en respuestas incorrectas.
     * @param {string} [config.optionPrefix="opcion"] - Prefijo utilizado para identificar las columnas de las opciones de respuesta.
     * @param {boolean} [config.randomizeQuestions=false] - Indica si las preguntas deben presentarse en orden aleatorio.
     * @param {boolean} [config.randomizeOptions=false] - Indica si las opciones dentro de una pregunta deben ordenarse aleatoriamente.
     * @param {number} [config.passingScore=80] - Puntaje mínimo necesario para aprobar el cuestionario, expresado en porcentaje.
     * @param {number} [config.defaultWeight=1] - Ponderación por defecto asignada a cada pregunta, si no se especifica en el archivo Excel.
     * @param {number} [config.maxQuestions=Infinity] - Número máximo de preguntas a incluir en el cuestionario. Si no se especifica, se incluyen todas las preguntas.
     * @param {number} [config.maxAttempts=Infinity] - Número máximo de intentos permitidos para completar el cuestionario. Por defecto, es infinito.
     * @license Licencia Comercial Propietaria
     * @version 1.0.0
     * @author Salvador Martínez
     * @copyright Este software está protegido por derechos de autor y es propiedad de E360 Digital Solutions S.A. de C.V.
     * El uso está restringido a los términos y condiciones establecidos en el contrato de licencia.
     * Para obtener una licencia y acceder a las características completas y soporte, contáctenos en licencias@espacio360.com.mx
     */
    constructor(config) {
      this.config = {
        excelFileUrl: config.excelFileUrl,
        mandatoryFields: config.mandatoryFields || {
          question: "pregunta",
          correctOption: "opcion_c",
          correctAnswers: "correctas",
          correctFeedback: "retroalimentacion_correcta",
          incorrectFeedback: "retroalimentacion_incorrecta",
        },
        optionPrefix: config.optionPrefix || "opcion",
        randomizeQuestions: config.randomizeQuestions || false,
        randomizeOptions: config.randomizeOptions || false,
        passingScore: config.passingScore || 80,
        defaultWeight: config.defaultWeight || 1,
        maxAttempts: config.maxAttempts || Infinity,
        maxQuestions: config.maxQuestions || Infinity,
      };
      this.questions = []; // Contendrá las preguntas seleccionadas para el cuestionario.
      this.currentQuestionIndex = 0; // Índice de la pregunta actual en el cuestionario.
      this.correctAnswersCount = 0; // Contador de respuestas correctas.
      this.incorrectAnswersCount = 0; // Contador de respuestas incorrectas.
      this.totalScore = 0; // Puntaje acumulado del cuestionario.
      this.attempts = 0; // Contador de intentos realizados.
    }

    /**
     * Carga las preguntas desde el archivo Excel especificado en la configuración.
     * @async
     * @returns {Promise<void>}
     */
    async loadQuestionsFromExcel() {
      const rawData = await QuizManager.readExcelFile(this.config.excelFileUrl);

      const sheetName = Object.keys(rawData)[0];
      const sheetData = rawData[sheetName];

      if (!Array.isArray(sheetData)) {
        console.error("The selected sheet does not contain an array of data.");
        return;
      }

      let questions = QuizManager.processQuestions(sheetData, this.config);

      if (this.config.randomizeQuestions) {
        questions = QuizManager.shuffle(questions);
      }

      // Validar maxQuestions solo si fue definido explícitamente
      const totalQuestions = questions.length;
      if (this.config.maxQuestions !== Infinity && this.config.maxQuestions > totalQuestions) {
        throw new Error(`El número máximo de preguntas especificado (${this.config.maxQuestions}) excede el total disponible (${totalQuestions}).`);
      }

      // Seleccionar las preguntas según maxQuestions
      this.questions = this.config.maxQuestions === Infinity ? questions : questions.slice(0, this.config.maxQuestions);
    }

    /**
     * Lee un archivo Excel y lo convierte a un objeto JSON.
     * @static
     * @async
     * @param {string} url - URL del archivo Excel.
     * @returns {Promise<Object>} Objeto donde las hojas del Excel son claves y sus datos son arrays de objetos.
     */
    static async readExcelFile(url) {
      try {
        const response = await fetch(url);
        const arrayBuffer = await response.arrayBuffer();
        const data = new Uint8Array(arrayBuffer);
        const workbook = XLSX.read(data, { type: "array" });

        const result = {};
        workbook.SheetNames.forEach((sheetName) => {
          const sheet = workbook.Sheets[sheetName];
          result[sheetName] = XLSX.utils.sheet_to_json(sheet);
        });
        return result;
      } catch (error) {
        console.error("Error reading Excel file:", error);
        throw error;
      }
    }

    /**
     * Procesa los datos de una hoja del Excel para generar las preguntas.
     * @static
     * @param {Array} rawData - Datos de una hoja en formato JSON.
     * @param {Object} config - Configuración para procesar preguntas.
     * @returns {Array} Array de preguntas procesadas.
     */
    static processQuestions(rawData, config) {
      return rawData
        .map((row) => {
          const fields = config.mandatoryFields;

          if (!row[fields.question]) {
            console.error(`Missing mandatory field: '${fields.question}'`);
            return null;
          }
          
          if (!row[fields.correctAnswers] && !row[fields.correctOption]) {
            console.error(`Missing correct answers field: need either '${fields.correctAnswers}' or '${fields.correctOption}'`);
            return null;
          }

          let options = Object.keys(row)
            .filter((key) => key.startsWith(config.optionPrefix))
            .map((key, index) => {
              let isCorrect = false;
              
              if (row[fields.correctAnswers]) {
                const correctIndices = row[fields.correctAnswers]
                  .toString()
                  .split(',')
                  .map(num => parseInt(num.trim()) - 1);
                isCorrect = correctIndices.includes(index);
              } else if (row[fields.correctOption]) {
                isCorrect = key === fields.correctOption;
              }
              
              return {
                id: key,
                text: row[key]?.trim(),
                isCorrect: isCorrect,
              };
            });

          const specialOptionsTexts = config.specialOptions || [];
          let specialOptions = [];
          let regularOptions = [];

          options.forEach((option) => {
            if (specialOptionsTexts.some((text) => option.text.toLowerCase().includes(text.toLowerCase()))) {
              specialOptions.push(option);
            } else {
              regularOptions.push(option);
            }
          });

          if (config.randomizeOptions) {
            regularOptions = QuizManager.shuffle(regularOptions);
          }

          options = [...regularOptions, ...specialOptions];

          const additionalInfo = Object.keys(row)
            .filter((key) => !Object.values(fields).includes(key) && !key.startsWith(config.optionPrefix))
            .reduce((info, key) => {
              // Convertir a cadena antes de aplicar .trim() y manejar valores nulos/indefinidos
              const value = row[key] !== null && row[key] !== undefined ? String(row[key]).trim() : "";
              info[key] = value;
              return info;
            }, {});

          const weight = parseFloat(row["ponderacion"]) || config.defaultWeight;

          return {
            question: row[fields.question].trim(),
            options,
            correctFeedback: row[fields.correctFeedback]?.trim() || "",
            incorrectFeedback: row[fields.incorrectFeedback]?.trim() || "",
            additionalInfo,
            weight,
          };
        })
        .filter(Boolean);
    }

    /**
     * Baraja un array usando el algoritmo Fisher-Yates.
     * @static
     * @param {Array} array - Array a barajar.
     * @returns {Array} Array barajado.
     */
    static shuffle(array) {
      for (let i = array.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
      }
      return array;
    }

    /**
     * Obtiene la pregunta actual.
     * @returns {Object} La pregunta actual.
     */
    getCurrentQuestion() {
      return this.questions[this.currentQuestionIndex];
    }

    /**
     * Avanza a la siguiente pregunta y la retorna.
     * @returns {Object|null} La siguiente pregunta o `null` si no hay más.
     */
    getNextQuestion() {
      if (this.currentQuestionIndex < this.questions.length - 1) {
        this.currentQuestionIndex++;
        return this.questions[this.currentQuestionIndex];
      }
      return null;
    }
    /**
     * Registra la respuesta del usuario a una pregunta específica por su ID.
     * @param {number} questionId - El ID de la pregunta a responder.
     * @param {boolean} isCorrect - Indica si la respuesta fue correcta.
     */
    answerQuestionById(questionId, isCorrect) {
      if (questionId < 0 || questionId >= this.questions.length) {
        console.error(`Invalid question ID: ${questionId}`);
        return;
      }

      const question = this.questions[questionId];

      if (!question.answered) {
        question.answered = true; // Marcar la pregunta como respondida
        question.isAnswerCorrect = isCorrect; //Guarda el resultado de la respuesta
        if (isCorrect) {
          this.correctAnswersCount++;
          this.totalScore += question.weight;
        } else {
          this.incorrectAnswersCount++;
        }
      } else {
        console.warn(`Question ID ${questionId} has already been answered.`);
      }
    }

    /**
     * Registra la respuesta del usuario a la pregunta actual.
     * @param {boolean} isCorrect - Indica si la respuesta fue correcta.
     */
    answerCurrentQuestion(isCorrect) {
      this.answerQuestionById(this.currentQuestionIndex, isCorrect);
    }

    /**
     * Registra la respuesta del usuario a la pregunta por su indice.
     * @param {boolean} isCorrect - Indica si la respuesta fue correcta.
     */
    answerCurrentQuestionById(index, isCorrect) {
      this.answerQuestionById(index, isCorrect);
    }

    /**
     * Verifica si hay más preguntas disponibles.
     * @returns {boolean} `true` si hay más preguntas, `false` de lo contrario.
     */
    hasMoreQuestions() {
      // Contamos cuántas preguntas han sido respondidas
      const answeredQuestions = this.questions.filter((q) => q.answered).length;

      // Si el número de preguntas respondidas es menor que el total, hay más preguntas
      return answeredQuestions < this.questions.length;
    }

    /**
     * Genera un resumen del estado actual del cuestionario.
     * @returns {Object} Resumen del cuestionario.
     */
    getSummary() {
      const totalQuestions = this.questions.length;
      const maxScore = this.questions.reduce((sum, q) => sum + q.weight, 0);
      const scorePercentage = Math.round((this.totalScore * 100) / maxScore);
      const passed = scorePercentage >= this.config.passingScore;

      return {
        correct: this.correctAnswersCount,
        incorrect: this.incorrectAnswersCount,
        total: totalQuestions,
        score: scorePercentage,
        maxScore,
        passed,
        attempts: this.attempts,
        maxAttempts: this.config.maxAttempts,
      };
    }

    /**
     * Genera un resumen del estado de un conjunto de preguntas.
     * @param {Set} questionSet - Conjunto de preguntas del cuestionario.
     * @returns {Object} Resumen del cuestionario.
     */
    generateSummaryFromSet(questionSet) {
      // Convertir el Set a un Array para procesarlo
      const questions = Array.from(questionSet);
      // Variables para calcular el resumen
      const totalQuestions = questions.length;
      const maxScore = questions.reduce((sum, q) => sum + (q.weight || 1), 0);
      const correctAnswersCount = questions.filter((q) => q.isAnswerCorrect).length;
      const incorrectAnswersCount = totalQuestions - correctAnswersCount;
      const totalScore = questions.reduce((sum, q) => (q.isAnswerCorrect ? sum + (q.weight || 1) : sum), 0);
      const scorePercentage = maxScore > 0 ? Math.round((totalScore * 100) / maxScore) : 0;
      const passed = scorePercentage >= this.config.passingScore;
      // Generar el resumen
      return {
        correct: correctAnswersCount,
        incorrect: incorrectAnswersCount,
        total: totalQuestions,
        score: scorePercentage,
        maxScore,
        passed,
        attempts: this.attempts,
        maxAttempts: this.config.maxAttempts,
      };
    }
    /**
     * Devuelve los datos necesarios para renderizar la pregunta actual.
     * @returns {Object|null} Datos de la pregunta actual o `null` si no existe.
     */
    getRenderData() {
      const currentQuestion = this.getCurrentQuestion();
      if (!currentQuestion) return null;

      return {
        text: currentQuestion.question,
        options: currentQuestion.options.map((option) => ({
          id: option.id,
          text: option.text,
          isCorrect: option.isCorrect,
        })),
        correctFeedback: currentQuestion.correctFeedback,
        incorrectFeedback: currentQuestion.incorrectFeedback,
        additionalInfo: currentQuestion.additionalInfo,
      };
    }

    /**
     * Obtiene una pregunta específica por ID.
     * @param {number} id - Índice de la pregunta.
     * @returns {Object|null} Pregunta correspondiente o `null` si el índice no es válido.
     */
    getQuestionById(id) {
      if (id < 0 || id >= this.questions.length) {
        console.error(`Invalid question ID: ${id}`);
        return null;
      }
      return this.questions[id];
    }

    /**
     * Genera los datos necesarios para renderizar una pregunta específica por ID.
     * @param {number} id - Índice de la pregunta.
     * @returns {Object|null} Datos de renderizado o `null` si el ID no es válido.
     */
    getRenderDataById(id) {
      const question = this.getQuestionById(id);
      if (!question) return null;

      return {
        text: question.question,
        options: question.options.map((option) => ({
          id: option.id,
          text: option.text,
          isCorrect: option.isCorrect,
        })),
        correctFeedback: question.correctFeedback,
        incorrectFeedback: question.incorrectFeedback,
        additionalInfo: question.additionalInfo,
      };
    }

    /**
     * Devuelve los datos necesarios para renderizar todas las preguntas.
     * @returns {Array} Array de objetos con los datos de todas las preguntas.
     */
    getAllRenderData() {
      return this.questions.map((question) => ({
        text: question.question,
        options: question.options.map((option) => ({
          id: option.id,
          text: option.text,
          isCorrect: option.isCorrect,
        })),
        correctFeedback: question.correctFeedback,
        incorrectFeedback: question.incorrectFeedback,
        additionalInfo: question.additionalInfo,
      }));
    }
    /**
     * Incrementa el número de intentos del cuestionario.
     */
    incrementAttempts() {
      this.attempts++;
    }

    /**
     * Obtiene el número de intentos actuales.
     * @returns {number} Número de intentos.
     */
    getAttempts() {
      return this.attempts;
    }
    /**
     * Establece el número de intentos desde una fuente externa (LMS, etc.).
     * @param {number} attempts - Número inicial de intentos.
     */
    setAttempts(attempts) {
      if (isNaN(attempts) || attempts < 0) {
        console.error("El valor de intentos no es válido.");
        return;
      }
      this.attempts = attempts;
    }

    /**
     * Verifica si el número máximo de intentos se ha alcanzado.
     * @returns {boolean} `true` si se alcanzó el máximo de intentos, `false` de lo contrario.
     */
    hasReachedMaxAttempts() {
      return this.attempts >= this.config.maxAttempts;
    }
  }

  // Exponer la clase al ámbito global
  global.QuizManager = QuizManager;
})(window);
