/**
 * PressPrimer Quiz - Frontend JavaScript
 *
 * Handles quiz interactions including starting quizzes, saving answers,
 * auto-save, timer management, and submission.
 *
 * @package PressPrimer_Quiz
 * @since 1.0.0
 */

(function($) {
	'use strict';

	/**
	 * Quiz Landing Page Handler
	 */
	const QuizLanding = {
		/**
		 * Initialize landing page functionality
		 */
		init: function() {
			this.bindEvents();
		},

		/**
		 * Bind event handlers
		 */
		bindEvents: function() {
			$(document).on('click', '.ppq-start-quiz-button', this.handleStartQuiz.bind(this));
			$(document).on('click', '.ppq-load-more', this.handleLoadMore.bind(this));
		},

		/**
		 * Handle Start Quiz button click
		 *
		 * @param {Event} e Click event
		 */
		handleStartQuiz: function(e) {
			e.preventDefault();

			const $button = $(e.currentTarget);
			const quizId = $button.data('quiz-id');

			// Validate quiz ID
			if (!quizId) {
				this.showError(pressprimerQuiz.strings.error);
				return;
			}

			// Check if user is guest and get email
			const $emailForm = $('.ppq-guest-email-form');
			const $emailInput = $('#ppq-guest-email');
			let guestEmail = '';
			const emailRequired = $emailForm.length && $emailForm.data('email-required') === 1;

			if ($emailInput.length) {
				guestEmail = $emailInput.val().trim();

				// Check if email is required
				if (emailRequired && !guestEmail) {
					this.showError(ppqQuiz.strings.emailRequired);
					$emailInput.focus();
					$emailInput.addClass('ppq-input-error');
					return;
				}

				// Validate email format if provided
				if (guestEmail && !this.isValidEmail(guestEmail)) {
					this.showError(pressprimerQuiz.strings.emailRequired);
					$emailInput.focus();
					$emailInput.addClass('ppq-input-error');
					return;
				}

				// Clear error state
				$emailInput.removeClass('ppq-input-error');
			}

			// Disable button and show loading state
			$button.prop('disabled', true).addClass('ppq-loading');

			// Make AJAX request to create attempt
			$.ajax({
				url: pressprimerQuiz.ajaxUrl,
				type: 'POST',
				data: {
					action: 'pressprimer_quiz_start_quiz',
					nonce: pressprimerQuiz.nonce,
					quiz_id: quizId,
					guest_email: guestEmail
				},
				success: (response) => {
					if (response.success && response.data.attempt_id) {
						// Redirect to quiz interface (include guest token if provided)
						const attemptUrl = this.buildAttemptUrl(
							response.data.attempt_id,
							response.data.guest_token || null
						);
						window.location.href = attemptUrl;
					} else {
						// Show error message
						const errorMessage = response.data && response.data.message
							? response.data.message
							: pressprimerQuiz.strings.error;
						this.showError(errorMessage);
						$button.prop('disabled', false).removeClass('ppq-loading');
					}
				},
				error: () => {
					this.showError(pressprimerQuiz.strings.error);
					$button.prop('disabled', false).removeClass('ppq-loading');
				}
			});
		},

		/**
		 * Validate email address
		 *
		 * @param {string} email Email to validate
		 * @return {boolean} True if valid
		 */
		isValidEmail: function(email) {
			// Basic email validation regex
			const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
			return re.test(email);
		},

		/**
		 * Build attempt URL
		 *
		 * @param {number} attemptId Attempt ID
		 * @param {string|null} guestToken Optional guest token for anonymous users
		 * @return {string} Attempt URL
		 */
		buildAttemptUrl: function(attemptId, guestToken) {
			const url = new URL(window.location.href);
			url.searchParams.set('attempt', attemptId);
			// Include guest token for anonymous access
			if (guestToken) {
				url.searchParams.set('token', guestToken);
			}
			// Remove retake flag - we're now in an active attempt
			url.searchParams.delete('pressprimer_quiz_retake');
			return url.toString();
		},

		/**
		 * Handle Load More attempts click
		 *
		 * @param {Event} e Click event
		 */
		handleLoadMore: function(e) {
			e.preventDefault();

			const $button = $(e.currentTarget);
			const $container = $button.closest('.ppq-previous-attempts');
			const $list = $container.find('.ppq-attempts-list');
			const quizId = $container.data('quiz-id');
			const total = $container.data('total');
			const offset = parseInt($button.data('offset'), 10) || 0;

			// Disable button and show loading
			$button.prop('disabled', true).attr('aria-busy', 'true').text(pressprimerQuiz.strings.loadingText || 'Loading...');
			$list.attr('aria-busy', 'true');

			$.ajax({
				url: pressprimerQuiz.ajaxUrl,
				type: 'POST',
				data: {
					action: 'pressprimer_quiz_load_more_attempts',
					nonce: pressprimerQuiz.nonce,
					quiz_id: quizId,
					offset: offset
				},
				success: function(response) {
					$list.removeAttr('aria-busy');
					if (response.success && response.data.html) {
						$list.append(response.data.html);

						// Announce loaded count to screen readers
						var $status = $container.find('#ppq-attempts-status');
						var loaded = response.data.loaded || 0;
						$status.text(loaded + ' more attempts loaded.');

						if (response.data.has_more) {
							$button.data('offset', response.data.next_offset);
							$button.prop('disabled', false).removeAttr('aria-busy');

							// Update button text with remaining count
							var remaining = total - response.data.next_offset;
							if (remaining > 0) {
								$button.text(
									(pressprimerQuiz.strings.showMore || 'Show more') +
									' (' + total + ' ' + (pressprimerQuiz.strings.total || 'total') + ')'
								);
							}
						} else {
							// No more attempts — remove the footer
							$button.closest('.ppq-attempts-footer').remove();
							$status.text('All ' + total + ' attempts loaded.');
						}
					}
				},
				error: function() {
					$list.removeAttr('aria-busy');
					$button.prop('disabled', false).removeAttr('aria-busy');
					$button.text(pressprimerQuiz.strings.showMore || 'Show more');
				}
			});
		},

		/**
		 * Show error message
		 *
		 * @param {string} message Error message
		 */
		showError: function(message) {
			// Remove any existing error notices
			$('.ppq-quiz-actions .ppq-notice-error').remove();

			// Create and insert error notice
			const $notice = $('<div>')
				.addClass('ppq-notice ppq-notice-error')
				.html('<p>' + this.escapeHtml(message) + '</p>')
				.hide()
				.prependTo('.ppq-quiz-actions')
				.slideDown(200);

			// Auto-remove after 5 seconds
			setTimeout(() => {
				$notice.slideUp(200, function() {
					$(this).remove();
				});
			}, 5000);
		},

		/**
		 * Escape HTML to prevent XSS
		 *
		 * @param {string} text Text to escape
		 * @return {string} Escaped text
		 */
		escapeHtml: function(text) {
			const map = {
				'&': '&amp;',
				'<': '&lt;',
				'>': '&gt;',
				'"': '&quot;',
				"'": '&#039;'
			};
			return text.replace(/[&<>"']/g, function(m) { return map[m]; });
		}
	};

	/**
	 * Quiz Interface Handler
	 *
	 * Handles the quiz-taking interface including:
	 * - Question navigation
	 * - Answer selection
	 * - Timer management
	 * - Quiz submission
	 */
	const QuizInterface = {
		// State
		$container: null,
		attemptId: null,
		quizId: null,
		currentQuestionIndex: 0,
		totalQuestions: 0,
		timeLimit: null,
		timeRemaining: null,
		timerInterval: null,
		isSubmitting: false,
		isAutoSubmit: false,

		// Auto-save state
		autoSaveTimer: null,
		autoSaveDelay: 1000, // 1 second delay
		pendingSaves: {},
		isSaving: false,

		// Active time tracking
		activeElapsedMs: 0,
		lastActiveTimestamp: null,
		isTabActive: true,
		heartbeatInterval: null,
		heartbeatDelay: 30000, // 30 seconds

		// Edge case handling
		timerPaused: false,
		hasUnsavedChanges: false,
		isOnline: true,

		// Timer warning tracking (for screen reader announcements)
		fiveMinuteWarningAnnounced: false,
		oneMinuteWarningAnnounced: false,

		// Assignment deadline tracking
		deadlineTimestamp: null,      // Unix timestamp when submission deadline passes
		allowLate: false,             // Whether late submissions are allowed
		autoSubmitDeadline: false,    // Whether to auto-submit when deadline passes
		deadlineRemaining: null,      // Seconds until deadline
		deadlineInterval: null,       // Interval for deadline countdown
		deadlineWarningAnnounced: false,
		isDeadlineSubmit: false,      // Track if submission was triggered by deadline
		isUsingDeadlineAsTimer: false, // Track if main timer is using deadline instead of time limit

		/**
		 * Initialize quiz interface
		 */
		init: function() {
			const self = this;

			this.$container = $('.ppq-quiz-interface');

			if (!this.$container.length) {
				return;
			}

			// Get quiz data
			this.attemptId = this.$container.data('attempt-id');
			this.quizId = this.$container.data('quiz-id');
			this.quizMode = this.$container.data('quiz-mode') || 'timed';
			this.allowSkip = this.$container.data('allow-skip') === 1 || this.$container.data('allow-skip') === '1';
			this.allowBackward = this.$container.data('allow-backward') === 1 || this.$container.data('allow-backward') === '1';
			this.pageMode = this.$container.data('page-mode') || 'single';
			this.questionsPerPage = parseInt(this.$container.data('questions-per-page'), 10) || 1;
			this.timeLimit = this.$container.data('time-limit');
			this.timeRemaining = this.$container.data('time-remaining');
			this.totalQuestions = $('.ppq-question').length;

			// Calculate total pages for paginated mode
			this.totalPages = this.pageMode === 'paged'
				? Math.ceil(this.totalQuestions / this.questionsPerPage)
				: this.totalQuestions;
			this.currentPage = 0;

			// Load previously accumulated active time (for resume)
			this.activeElapsedMs = parseInt(this.$container.data('active-elapsed-ms'), 10) || 0;

			// Tutorial mode tracking
			this.feedbackShown = {}; // Track which questions have shown feedback
			this.isCheckingAnswer = false; // Prevent double-clicks on Check button

			// Restore checked state from server data (for page reload/resume)
			if (this.quizMode === 'tutorial') {
				this.$container.find('.ppq-question[data-answer-checked="1"]').each(function() {
					var qi = parseInt($(this).data('question-index'), 10);
					self.feedbackShown[qi] = true;
				});
			}

			// Get guest token from URL (for environments where cookies may not work)
			this.guestToken = new URLSearchParams(window.location.search).get('token') || '';

			// Get start question (first unanswered for resume, default 0)
			var startQuestion = parseInt(this.$container.data('start-question'), 10) || 0;
			// Convert to page number for paginated mode
			if (this.pageMode === 'paged') {
				startQuestion = Math.floor(startQuestion / this.questionsPerPage);
			}

			// Initialize components
			this.bindEvents();
			this.bindEdgeCaseHandlers();
			this.syncSelectionState();
			this.updateProgress();
			this.updateNavButtons();

			// Assignment deadline tracking (set by Educator addon via data attributes)
			this.deadlineTimestamp = parseInt(this.$container.data('deadline-timestamp'), 10) || null;
			this.allowLate = this.$container.data('allow-late') === 1 || this.$container.data('allow-late') === '1';
			this.autoSubmitDeadline = this.$container.data('auto-submit-deadline') === 1 || this.$container.data('auto-submit-deadline') === '1';

			// Start active time tracking
			this.startActiveTimeTracking();

			// Determine effective time remaining considering both quiz time limit and assignment deadline
			this.initEffectiveTimer();

			// Show starting question (first unanswered on resume, or first question)
			this.showQuestion(startQuestion);

			// In tutorial mode, show Check button on current question if not already checked
			if (this.quizMode === 'tutorial') {
				this.updateCheckButtonVisibility();
			}

			// Create screen reader live region for announcements
			this.createScreenReaderRegion();
		},

		/**
		 * Create a live region for screen reader announcements
		 */
		createScreenReaderRegion: function() {
			if ($('#ppq-sr-announcements').length === 0) {
				$('body').append(
					'<div id="ppq-sr-announcements" class="ppq-sr-only" role="status" aria-live="polite" aria-atomic="true"></div>'
				);
			}
		},

		/**
		 * Announce a message to screen readers
		 *
		 * @param {string} message The message to announce
		 * @param {string} priority 'polite' or 'assertive'
		 */
		announceToScreenReader: function(message, priority) {
			priority = priority || 'polite';
			const $region = $('#ppq-sr-announcements');

			// Set the appropriate aria-live value
			$region.attr('aria-live', priority);

			// Clear and set message (clearing first ensures the message is announced even if identical)
			$region.text('');
			setTimeout(function() {
				$region.text(message);
			}, 100);
		},

		/**
		 * Sync visual selection state with actual input state
		 *
		 * Ensures ppq-selected class matches checked inputs on all questions.
		 */
		syncSelectionState: function() {
			$('.ppq-answer-input').each(function() {
				const $input = $(this);
				const $option = $input.closest('.ppq-answer-option');

				if ($input.is(':checked')) {
					$option.addClass('ppq-selected');
				} else {
					$option.removeClass('ppq-selected');
				}
			});
		},

		/**
		 * Bind event handlers
		 */
		bindEvents: function() {
			const self = this;

			// Navigation buttons
			$('#ppq-prev-button').on('click', function(e) {
				e.preventDefault();
				self.previousQuestion();
			});

			$('#ppq-next-button').on('click', function(e) {
				e.preventDefault();
				self.nextQuestion();
			});

			// Submit button
			$('#ppq-submit-button').on('click', function(e) {
				e.preventDefault();
				self.submitQuiz();
			});

			// Answer selection
			$(document).on('change', '.ppq-answer-input', function() {
				self.handleAnswerChange($(this));
			});

			// Confidence checkbox
			$(document).on('change', '.ppq-confidence-input', function() {
				self.handleConfidenceChange($(this));
			});

			// Handle clicks on answer options.
			// The input is visually hidden (opacity: 0, w/h: 0), so clicks on the
			// visible parts (custom radio/checkbox and text) are delegated here.
			$(document).on('click', '.ppq-answer-option', function(e) {
				// If clicking directly on the input, let native behavior handle it.
				if (e.target.tagName === 'INPUT') {
					return;
				}

				// If the click landed on (or inside) a link or button within
				// rich answer content, let that element's behavior run instead
				// of selecting the answer.
				if (e.target.closest('a, button')) {
					return;
				}

				const $input = $(this).find('.ppq-answer-input');

				if ($input.attr('type') === 'checkbox') {
					// Toggle checkbox
					$input.prop('checked', !$input.prop('checked'));
				} else {
					// Select radio
					$input.prop('checked', true);
				}

				// Call handler directly instead of triggering synthetic event
				self.handleAnswerChange($input);
			});

			// Keyboard navigation
			$(document).on('keydown', function(e) {
				// Left arrow - previous (only if button is visible and not disabled)
				if (e.key === 'ArrowLeft' && $('#ppq-prev-button').is(':visible') && !$('#ppq-prev-button').prop('disabled')) {
					e.preventDefault();
					self.previousQuestion();
				}
				// Right arrow - next
				else if (e.key === 'ArrowRight' && $('#ppq-next-button').is(':visible')) {
					e.preventDefault();
					self.nextQuestion();
				}
			});

			// Tutorial mode: Continue button after feedback
			$(document).on('click', '.ppq-continue-button', function(e) {
				e.preventDefault();
				self.handleContinueAfterFeedback($(this));
			});

			// Tutorial mode: Check Answer button
			$(document).on('click', '.ppq-check-answer-button', function(e) {
				e.preventDefault();
				self.handleCheckAnswer($(this));
			});
		},

		/**
		 * Handle continue button click after feedback (tutorial mode)
		 *
		 * @param {jQuery} $button The continue button
		 */
		handleContinueAfterFeedback: function($button) {
			const $question = $button.closest('.ppq-question');
			const questionIndex = $question.data('question-index');

			// Hide feedback
			$question.find('.ppq-feedback').slideUp(200);

			// Show navigation buttons again
			$('#ppq-prev-button').show();

			// Check if all questions on current page have been checked (for paged mode)
			let allPageQuestionsChecked = true;
			let isLastPage = false;

			if (this.pageMode === 'paged') {
				const startIndex = this.currentPage * this.questionsPerPage;
				const endIndex = Math.min(startIndex + this.questionsPerPage, this.totalQuestions);
				isLastPage = endIndex >= this.totalQuestions;

				for (let i = startIndex; i < endIndex; i++) {
					if (!this.feedbackShown[i]) {
						allPageQuestionsChecked = false;
						break;
					}
				}
			} else {
				// Single question mode - check if this is the last question
				isLastPage = questionIndex === this.totalQuestions - 1;
				allPageQuestionsChecked = true;
			}

			// If this is the last question overall, show submit button
			if (questionIndex === this.totalQuestions - 1) {
				$('#ppq-submit-button').show();
				$('#ppq-next-button').hide();
			} else if (this.pageMode === 'paged' && !allPageQuestionsChecked) {
				// More unchecked questions on this page - don't advance, just update visibility
				// Navigation buttons are already shown, user can continue with other questions
				this.updateCheckButtonVisibility();
			} else if (this.pageMode === 'paged' && allPageQuestionsChecked && isLastPage) {
				// All questions on last page checked - show submit
				$('#ppq-submit-button').show();
				$('#ppq-next-button').hide();
			} else {
				// Show next button and auto-advance to next question/page
				$('#ppq-next-button').show();
				this.nextQuestion();
			}
		},

		/**
		 * Bind edge case handlers
		 */
		bindEdgeCaseHandlers: function() {
			const self = this;

			// Warn before leaving page with unsaved changes
			$(window).on('beforeunload', function(e) {
				// Don't warn if quiz is being submitted
				if (self.isSubmitting) {
					return undefined;
				}

				// Don't warn if no unsaved changes
				if (!self.hasUnsavedChanges && Object.keys(self.pendingSaves).length === 0) {
					return undefined;
				}

				// Show warning
				const message = pressprimerQuiz.strings.unsavedChanges || 'You have unsaved answers. Are you sure you want to leave?';
				e.returnValue = message;
				return message;
			});

			// Handle visibility change (tab switching)
			$(document).on('visibilitychange', function() {
				if (document.hidden) {
					self.handleTabHidden();
				} else {
					self.handleTabVisible();
				}
			});

			// Handle online/offline status
			$(window).on('online', function() {
				self.handleOnline();
			});

			$(window).on('offline', function() {
				self.handleOffline();
			});

			// Prevent back button during quiz
			if (window.history && window.history.pushState) {
				// Add a dummy state
				window.history.pushState('quiz-active', null, window.location.href);

				// Flag to track intentional navigation
				var isLeavingQuiz = false;

				// Handle popstate (back button)
				$(window).on('popstate', function(e) {
					// Skip if we're already leaving or submitting
					if (isLeavingQuiz || self.isSubmitting) {
						return;
					}

					// Push state back to prevent navigation
					window.history.pushState('quiz-active', null, window.location.href);

					// Confirm if user wants to abandon quiz
					if (confirm(pressprimerQuiz.strings.confirmLeave || 'Are you sure you want to leave this quiz? Your progress is saved, but you can only resume if the time limit allows.')) {
						// Set flag to prevent loop and allow navigation
						isLeavingQuiz = true;
						window.history.back();
					}
				});
			}
		},

		/**
		 * Handle tab hidden (user switched away)
		 */
		handleTabHidden: function() {
			// Pause active time tracking
			this.pauseActiveTimeTracking();

			// Force save any pending answers (includes active time)
			if (Object.keys(this.pendingSaves).length > 0) {
				if (this.autoSaveTimer) {
					clearTimeout(this.autoSaveTimer);
				}
				this.performAutoSave();
			} else {
				// No pending answers, but sync the active time using beacon API
				// (more reliable when tab is hidden, especially in Safari)
				this.syncActiveTimeBeacon();
			}
		},

		/**
		 * Handle tab visible (user switched back)
		 */
		handleTabVisible: function() {
			// Resume active time tracking
			this.resumeActiveTimeTracking();

			// Check if quiz timed out while tab was hidden
			if (this.timeLimit && this.timeRemaining <= 0) {
				this.handleTimeExpired();
			}
		},

		/**
		 * Handle online status restored
		 */
		handleOnline: function() {
			this.isOnline = true;

			// Retry pending saves
			if (Object.keys(this.pendingSaves).length > 0) {
				this.performAutoSave();
			}

			// Update indicator
			$('.ppq-offline-indicator').fadeOut();
		},

		/**
		 * Handle offline status
		 */
		handleOffline: function() {
			this.isOnline = false;

			// Show offline indicator
			let $indicator = $('.ppq-offline-indicator');
			if (!$indicator.length) {
				const offlineMsg = pressprimerQuiz.strings.offlineMessage || 'You are offline. Answers will be saved when connection is restored.';
				$indicator = $('<div class="ppq-offline-indicator">' + offlineMsg + '</div>')
					.appendTo('body');
			}
			$indicator.fadeIn();
		},

		/**
		 * Show specific question or page
		 *
		 * @param {number} index Question index (single mode) or page index (paginated mode)
		 */
		showQuestion: function(index) {
			if (this.pageMode === 'paged') {
				this.showPage(index);
			} else {
				this.showSingleQuestion(index);
			}
		},

		/**
		 * Show a single question (single question per page mode)
		 *
		 * @param {number} index Question index (0-based)
		 */
		showSingleQuestion: function(index) {
			if (index < 0 || index >= this.totalQuestions) {
				return;
			}

			// Hide all questions
			$('.ppq-question').hide();

			// Show current question
			const $question = $('.ppq-question[data-question-index="' + index + '"]');
			$question.fadeIn(200);

			// Update current index
			this.currentQuestionIndex = index;
			this.currentPage = index;

			// Update UI
			this.updateProgress();
			this.updateNavButtons();

			// In tutorial mode, update Check button visibility
			if (this.quizMode === 'tutorial') {
				this.updateCheckButtonVisibility();
			}

			// Announce question change to screen readers
			const questionNumber = index + 1;
			this.announceToScreenReader(
				(pressprimerQuiz.strings.questionOf || 'Question {current} of {total}')
					.replace('{current}', questionNumber)
					.replace('{total}', this.totalQuestions)
			);

			// Move focus to the question for keyboard users
			$question.attr('tabindex', '-1').focus();
		},

		/**
		 * Show a page of questions (paginated mode)
		 *
		 * @param {number} pageIndex Page index (0-based)
		 */
		showPage: function(pageIndex) {
			if (pageIndex < 0 || pageIndex >= this.totalPages) {
				return;
			}

			// Hide all questions
			$('.ppq-question').hide();

			// Calculate which questions to show
			const startIndex = pageIndex * this.questionsPerPage;
			const endIndex = Math.min(startIndex + this.questionsPerPage, this.totalQuestions);

			// Show questions for this page
			for (let i = startIndex; i < endIndex; i++) {
				$('.ppq-question[data-question-index="' + i + '"]').fadeIn(200);
			}

			// Update current page and question index (use first question on page)
			this.currentPage = pageIndex;
			this.currentQuestionIndex = startIndex;

			// Update UI
			this.updateProgress();
			this.updateNavButtons();

			// In tutorial mode, update Check button visibility for all visible questions
			if (this.quizMode === 'tutorial') {
				this.updateCheckButtonVisibility();
			}

			// Announce page change to screen readers
			const pageNumber = pageIndex + 1;
			this.announceToScreenReader(
				(pressprimerQuiz.strings.pageOf || 'Page {current} of {total}')
					.replace('{current}', pageNumber)
					.replace('{total}', this.totalPages)
			);

			// Move focus to the first question on page for keyboard users
			$('.ppq-question[data-question-index="' + startIndex + '"]').attr('tabindex', '-1').focus();
		},

		/**
		 * Navigate to previous question/page
		 */
		previousQuestion: function() {
			const canGoBack = this.pageMode === 'paged'
				? this.currentPage > 0
				: this.currentQuestionIndex > 0;

			if (canGoBack) {
				// Check if backward navigation is allowed
				if (!this.allowBackward) {
					this.showBackwardNotAllowedMessage();
					return;
				}

				if (this.pageMode === 'paged') {
					this.showPage(this.currentPage - 1);
				} else {
					this.showSingleQuestion(this.currentQuestionIndex - 1);
				}
			}
		},

		/**
		 * Navigate to next question/page
		 */
		nextQuestion: function() {
			if (this.pageMode === 'paged') {
				if (this.currentPage < this.totalPages - 1) {
					this.showPage(this.currentPage + 1);
				}
			} else {
				if (this.currentQuestionIndex < this.totalQuestions - 1) {
					this.showSingleQuestion(this.currentQuestionIndex + 1);
				}
			}
		},

		/**
		 * Update progress indicator
		 */
		updateProgress: function() {
			if (this.pageMode === 'paged') {
				const pageNumber = this.currentPage + 1;
				const percentComplete = (pageNumber / this.totalPages) * 100;

				// Update page number display
				$('.ppq-current-question').text(pageNumber);

				// Update progress bar
				$('#ppq-progress-bar').css('width', percentComplete + '%');
			} else {
				const questionNumber = this.currentQuestionIndex + 1;
				const percentComplete = (questionNumber / this.totalQuestions) * 100;

				// Update question number
				$('.ppq-current-question').text(questionNumber);

				// Update progress bar
				$('#ppq-progress-bar').css('width', percentComplete + '%');
			}
		},

		/**
		 * Update navigation button states
		 */
		updateNavButtons: function() {
			const $prevButton = $('#ppq-prev-button');
			const $nextButton = $('#ppq-next-button');
			const $submitButton = $('#ppq-submit-button');

			const isFirstPage = this.pageMode === 'paged' ? this.currentPage === 0 : this.currentQuestionIndex === 0;
			const isLastPage = this.pageMode === 'paged'
				? this.currentPage === this.totalPages - 1
				: this.currentQuestionIndex === this.totalQuestions - 1;

			// Previous button - hide if backward navigation not allowed, disable on first page/question
			if (!this.allowBackward) {
				$prevButton.hide();
			} else if (isFirstPage) {
				$prevButton.show().prop('disabled', true);
			} else {
				$prevButton.show().prop('disabled', false);
			}

			// On last page/question - show submit, hide next
			if (isLastPage) {
				$nextButton.hide();
				$submitButton.show();
			} else {
				$nextButton.show();
				$submitButton.hide();
			}

			// If skip not allowed, disable Next button when questions are unanswered
			this.updateNextButtonState();
		},

		/**
		 * Update Next button enabled/disabled state based on skip setting
		 */
		updateNextButtonState: function() {
			if (this.allowSkip) {
				return; // Skip is allowed, no restrictions
			}

			const $nextButton = $('#ppq-next-button');

			// Check all visible questions on current page
			let allAnswered = true;
			if (this.pageMode === 'paged') {
				const startIndex = this.currentPage * this.questionsPerPage;
				const endIndex = Math.min(startIndex + this.questionsPerPage, this.totalQuestions);
				for (let i = startIndex; i < endIndex; i++) {
					const $question = $('.ppq-question[data-question-index="' + i + '"]');
					if ($question.find('.ppq-answer-input:checked').length === 0) {
						allAnswered = false;
						break;
					}
				}
			} else {
				const $currentQuestion = $('.ppq-question[data-question-index="' + this.currentQuestionIndex + '"]');
				allAnswered = $currentQuestion.find('.ppq-answer-input:checked').length > 0;
			}

			if (allAnswered) {
				$nextButton.prop('disabled', false).removeAttr('title');
			} else {
				const tooltip = this.pageMode === 'paged'
					? (pressprimerQuiz.strings.skipNotAllowedTooltipPage || 'You must answer all questions on this page to proceed.')
					: (pressprimerQuiz.strings.skipNotAllowedTooltip || 'You must answer this question to proceed.');
				$nextButton.prop('disabled', true).attr('title', tooltip);
			}
		},

		/**
		 * Handle answer selection change
		 *
		 * @param {jQuery} $input The input that changed
		 */
		handleAnswerChange: function($input) {
			const $question = $input.closest('.ppq-question');
			const questionType = $question.data('question-type');
			const questionIndex = $question.data('question-index');
			const $option = $input.closest('.ppq-answer-option');

			// If feedback is already shown for this question (tutorial mode), ignore changes
			if (this.feedbackShown[questionIndex]) {
				return;
			}

			// For radio buttons (MC/TF) - remove other selections
			if ($input.attr('type') === 'radio') {
				$question.find('.ppq-answer-option').removeClass('ppq-selected');
				if ($input.is(':checked')) {
					$option.addClass('ppq-selected');
				}
			}
			// For checkboxes (MA) - toggle selection
			else if ($input.attr('type') === 'checkbox') {
				if ($input.is(':checked')) {
					$option.addClass('ppq-selected');
				} else {
					$option.removeClass('ppq-selected');
				}
			}

			// Mark as having unsaved changes
			this.hasUnsavedChanges = true;

			// Trigger auto-save
			this.triggerAutoSave($input);

			// In tutorial mode, update Check button visibility when answer changes
			if (this.quizMode === 'tutorial' && !this.feedbackShown[questionIndex]) {
				this.updateCheckButtonVisibility();
			}

			// Update Next button state if skip is not allowed
			if (!this.allowSkip) {
				this.updateNextButtonState();
			}
		},

		/**
		 * Handle confidence checkbox change
		 *
		 * @param {jQuery} $input The confidence checkbox input
		 */
		handleConfidenceChange: function($input) {
			const itemId = $input.data('item-id');

			// Validate item ID exists
			if (itemId === undefined || itemId === null || itemId === '') {
				return;
			}

			const isConfident = $input.is(':checked');

			// Mark as having unsaved changes
			this.hasUnsavedChanges = true;

			// Store confidence value for this item
			if (!this.pendingConfidence) {
				this.pendingConfidence = {};
			}
			this.pendingConfidence[itemId] = isConfident;

			// Trigger auto-save (will include confidence when saving)
			this.triggerConfidenceSave(itemId);
		},

		/**
		 * Trigger auto-save specifically for confidence changes
		 *
		 * @param {number} itemId The item ID
		 */
		triggerConfidenceSave: function(itemId) {
			const self = this;

			// Clear existing timer
			if (this.autoSaveTimer) {
				clearTimeout(this.autoSaveTimer);
			}

			// Set new timer
			this.autoSaveTimer = setTimeout(function() {
				self.performAutoSave();
			}, this.autoSaveDelay);
		},

		/**
		 * Update Check button visibility for visible questions (tutorial mode)
		 *
		 * Shows Check button for each visible question that has at least one answer
		 * selected and hasn't been checked yet. Handles multiple questions per page.
		 */
		updateCheckButtonVisibility: function() {
			if (this.quizMode !== 'tutorial') {
				return;
			}

			const self = this;

			// Get all visible questions on the current page
			let visibleQuestions = [];
			if (this.pageMode === 'paged') {
				const startIndex = this.currentPage * this.questionsPerPage;
				const endIndex = Math.min(startIndex + this.questionsPerPage, this.totalQuestions);
				for (let i = startIndex; i < endIndex; i++) {
					visibleQuestions.push(i);
				}
			} else {
				visibleQuestions.push(this.currentQuestionIndex);
			}

			// Update Check button for each visible question
			visibleQuestions.forEach(function(questionIndex) {
				const $question = $('.ppq-question[data-question-index="' + questionIndex + '"]');
				const $checkContainer = $question.find('.ppq-check-answer-container');

				// If already checked, hide the button
				if (self.feedbackShown[questionIndex]) {
					$checkContainer.hide();
					return;
				}

				// Check if any answer is selected
				const hasAnswer = $question.find('.ppq-answer-input:checked').length > 0;

				if (hasAnswer) {
					$checkContainer.removeClass('ppq-hidden').slideDown(200);
				} else {
					$checkContainer.slideUp(200);
				}
			});
		},

		/**
		 * Handle Check Answer button click (tutorial mode)
		 *
		 * Submits the answer to the server for validation and displays feedback.
		 *
		 * @param {jQuery} $button The check answer button
		 */
		handleCheckAnswer: function($button) {
			const self = this;

			// Prevent double-clicks
			if (this.isCheckingAnswer) {
				return;
			}

			const $question = $button.closest('.ppq-question');
			const questionIndex = $question.data('question-index');
			const itemId = $question.data('item-id');

			// Validate item ID exists
			if (itemId === undefined || itemId === null || itemId === '') {
				return;
			}

			// Check if already checked
			if (this.feedbackShown[questionIndex]) {
				return;
			}

			// Get selected answers
			let selectedAnswers = [];
			$question.find('.ppq-answer-input:checked').each(function() {
				selectedAnswers.push(parseInt($(this).val(), 10));
			});

			// Require at least one answer
			if (selectedAnswers.length === 0) {
				return;
			}

			// Set checking state
			this.isCheckingAnswer = true;
			$button.prop('disabled', true).text(pressprimerQuiz.strings.checking || 'Checking...');

			// Submit to server
			$.ajax({
				url: pressprimerQuiz.ajaxUrl,
				type: 'POST',
				data: {
					action: 'pressprimer_quiz_check_answer',
					nonce: pressprimerQuiz.nonce,
					attempt_id: this.attemptId,
					item_id: itemId,
					answers: selectedAnswers,
					guest_token: this.guestToken
				},
				success: function(response) {
					self.isCheckingAnswer = false;

					if (response.success) {
						self.showQuestionFeedback($question, response.data);
					} else {
						// Re-enable button on error
						$button.prop('disabled', false).text(pressprimerQuiz.strings.checkAnswer || 'Check Answer');
						alert(response.data.message || pressprimerQuiz.strings.error);
					}
				},
				error: function() {
					self.isCheckingAnswer = false;
					$button.prop('disabled', false).text(pressprimerQuiz.strings.checkAnswer || 'Check Answer');
					alert(pressprimerQuiz.strings.error);
				}
			});
		},

		/**
		 * Show feedback for a question after server validation (tutorial mode)
		 *
		 * @param {jQuery} $question The question element
		 * @param {Object} data Response data from server
		 */
		showQuestionFeedback: function($question, data) {
			const questionIndex = $question.data('question-index');

			// Mark feedback as shown
			this.feedbackShown[questionIndex] = true;

			// Hide Check button
			$question.find('.ppq-check-answer-container').hide();

			// Disable answer inputs
			$question.find('.ppq-answer-input').prop('disabled', true);
			$question.addClass('ppq-feedback-shown');

			// Show correct/incorrect styling on answers
			$question.find('.ppq-answer-option').each(function() {
				const $opt = $(this);
				const answerIndex = parseInt($opt.find('.ppq-answer-input').val(), 10);
				const isThisCorrect = data.correct_indices.indexOf(answerIndex) !== -1;
				const isSelected = $opt.hasClass('ppq-selected');

				if (isThisCorrect) {
					$opt.addClass('ppq-correct');
				} else if (isSelected && !isThisCorrect) {
					$opt.addClass('ppq-incorrect');
				}
			});

			// Build feedback display
			const $feedback = $question.find('.ppq-feedback');
			const resultClass = data.is_correct ? 'ppq-feedback-correct' : 'ppq-feedback-incorrect';
			const resultText = data.is_correct
				? (pressprimerQuiz.strings.correct || 'Correct!')
				: (pressprimerQuiz.strings.incorrect || 'Incorrect');

			$feedback.find('.ppq-feedback-result')
				.removeClass('ppq-feedback-correct ppq-feedback-incorrect')
				.addClass(resultClass)
				.html('<strong>' + resultText + '</strong>');

			// Show feedback text if provided
			const $feedbackText = $feedback.find('.ppq-feedback-text');
			if (data.feedback_text) {
				$feedbackText.html(data.feedback_text).show();
			} else {
				$feedbackText.hide();
			}

			// Show feedback container (remove ppq-hidden class first due to !important)
			$feedback.removeClass('ppq-hidden').slideDown(300);

			// Hide navigation buttons while feedback is shown
			$('#ppq-next-button, #ppq-prev-button, #ppq-submit-button').hide();
		},

		/**
		 * Trigger auto-save with debouncing
		 *
		 * @param {jQuery} $input The input that changed
		 */
		triggerAutoSave: function($input) {
			const self = this;
			const itemId = $input.data('item-id');
			const $question = $input.closest('.ppq-question');

			// Validate item ID exists
			if (itemId === undefined || itemId === null || itemId === '') {
				return;
			}

			// Get selected answer(s)
			let selectedAnswers = [];
			if ($input.attr('type') === 'radio') {
				const checkedValue = $question.find('.ppq-answer-input:checked').val();
				if (checkedValue !== undefined) {
					selectedAnswers = [parseInt(checkedValue, 10)];
				}
			} else if ($input.attr('type') === 'checkbox') {
				$question.find('.ppq-answer-input:checked').each(function() {
					selectedAnswers.push(parseInt($(this).val(), 10));
				});
			}

			// Add to pending saves
			this.pendingSaves[itemId] = selectedAnswers;

			// Clear existing timer
			if (this.autoSaveTimer) {
				clearTimeout(this.autoSaveTimer);
			}

			// Set new timer
			this.autoSaveTimer = setTimeout(function() {
				self.performAutoSave();
			}, this.autoSaveDelay);
		},

		/**
		 * Perform auto-save of pending answers
		 */
		performAutoSave: function() {
			const self = this;

			// Check if already saving
			if (this.isSaving) {
				// Retry after delay
				setTimeout(function() {
					self.performAutoSave();
				}, 500);
				return;
			}

			// Check if there are pending saves (answers or confidence)
			const hasAnswers = Object.keys(this.pendingSaves).length > 0;
			const hasConfidence = this.pendingConfidence && Object.keys(this.pendingConfidence).length > 0;

			if (!hasAnswers && !hasConfidence) {
				return;
			}

			// Get pending saves and clear queues
			const saves = Object.assign({}, this.pendingSaves);
			this.pendingSaves = {};

			const confidenceSaves = this.pendingConfidence ? Object.assign({}, this.pendingConfidence) : {};
			this.pendingConfidence = {};

			// Set saving state
			this.isSaving = true;
			this.showAutoSaveIndicator('saving');

			// Get current active elapsed time
			const activeElapsedMs = this.getCurrentActiveElapsedMs();

			// Build form data manually to ensure proper array serialization
			const formData = {
				action: 'pressprimer_quiz_save_answers',
				nonce: pressprimerQuiz.nonce,
				attempt_id: this.attemptId,
				active_elapsed_ms: activeElapsedMs,
				guest_token: this.guestToken
			};

			// Add answers with proper array notation for PHP
			Object.keys(saves).forEach(function(itemId) {
				const answers = saves[itemId];
				if (Array.isArray(answers) && answers.length > 0) {
					answers.forEach(function(answerId, index) {
						formData['answers[' + itemId + '][' + index + ']'] = answerId;
					});
				} else if (Array.isArray(answers) && answers.length === 0) {
					// Empty array - send empty marker so PHP knows to clear answers
					formData['answers[' + itemId + '][]'] = '';
				}
			});

			// Add confidence values
			Object.keys(confidenceSaves).forEach(function(itemId) {
				formData['confidence[' + itemId + ']'] = confidenceSaves[itemId] ? '1' : '0';
			});

			// Make AJAX request
			$.ajax({
				url: pressprimerQuiz.ajaxUrl,
				type: 'POST',
				data: formData,
				success: function(response) {
					self.isSaving = false;

					if (response.success) {
						// Clear unsaved changes flag
						self.hasUnsavedChanges = false;
						self.showAutoSaveIndicator('saved');
					} else {
						// Save failed - add back to queues
						Object.assign(self.pendingSaves, saves);
						if (!self.pendingConfidence) {
							self.pendingConfidence = {};
						}
						Object.assign(self.pendingConfidence, confidenceSaves);
						// Show more descriptive error message
						const errorMsg = response.data && response.data.message
							? response.data.message
							: pressprimerQuiz.strings.saveFailed;
						self.showAutoSaveIndicator('failed', errorMsg);
					}
				},
				error: function() {
					self.isSaving = false;
					// Save failed - add back to queues
					Object.assign(self.pendingSaves, saves);
					if (!self.pendingConfidence) {
						self.pendingConfidence = {};
					}
					Object.assign(self.pendingConfidence, confidenceSaves);
					self.showAutoSaveIndicator('failed');

					// Retry after delay
					setTimeout(function() {
						self.performAutoSave();
					}, 2000);
				}
			});
		},

		/**
		 * Show auto-save indicator
		 *
		 * @param {string} state State: 'saving', 'saved', 'failed'
		 * @param {string} message Optional custom message for failed state
		 */
		showAutoSaveIndicator: function(state, message) {
			const self = this;
			const $indicator = $('#ppq-autosave-indicator');
			const $text = $indicator.find('.ppq-autosave-text');
			const $icon = $indicator.find('.ppq-autosave-icon');

			// Update text and icon based on state
			if (state === 'saving') {
				$text.text(pressprimerQuiz.strings.saving);
				$icon.text('💾');
				$indicator.removeClass('ppq-autosave-error').addClass('ppq-autosave-saving');
			} else if (state === 'saved') {
				$text.text(pressprimerQuiz.strings.saved);
				$icon.text('✓');
				$indicator.removeClass('ppq-autosave-saving ppq-autosave-error');
				// Announce save success to screen readers
				self.announceToScreenReader(pressprimerQuiz.strings.saved || 'Answer saved');
			} else if (state === 'failed') {
				// Use custom message if provided, otherwise use default
				const errorMsg = message || pressprimerQuiz.strings.saveFailed;
				$text.text(errorMsg);
				$icon.text('⚠️');
				$indicator.removeClass('ppq-autosave-saving').addClass('ppq-autosave-error');
				// Announce error to screen readers (assertive for errors)
				self.announceToScreenReader(errorMsg, 'assertive');
			}

			// Show indicator
			$indicator.fadeIn(200).addClass('ppq-show');

			// Hide after delay (except for errors)
			if (state !== 'failed') {
				setTimeout(function() {
					$indicator.removeClass('ppq-show').fadeOut(200);
				}, 2000);
			}
		},

		/**
		 * Start countdown timer
		 */
		startTimer: function() {
			const self = this;
			const $timer = $('#ppq-timer');
			const $timerContainer = $timer.closest('.ppq-timer-container');

			// Update timer every second
			this.timerInterval = setInterval(function() {
				self.timeRemaining--;

				// Update display
				const minutes = Math.floor(self.timeRemaining / 60);
				const seconds = self.timeRemaining % 60;
				const timeString = self.padZero(minutes) + ':' + self.padZero(seconds);
				$timer.text(timeString);

				// Update timer styling and announce warnings
				// Add classes to both timer and container (for theme compatibility)
				if (self.timeRemaining <= 60) {
					// Last minute - danger (red, fast pulse)
					$timer.removeClass('ppq-timer-warning').addClass('ppq-timer-danger');
					$timerContainer.removeClass('ppq-timer-warning').addClass('ppq-timer-danger');

					// Announce one minute warning to screen readers (once)
					if (!self.oneMinuteWarningAnnounced) {
						self.oneMinuteWarningAnnounced = true;
						self.announceToScreenReader(
							pressprimerQuiz.strings.oneMinuteWarning || 'Warning: One minute remaining!',
							'assertive'
						);
					}
				} else if (self.timeRemaining <= 300) {
					// Last 5 minutes - warning (orange, slow pulse)
					$timer.addClass('ppq-timer-warning').removeClass('ppq-timer-danger');
					$timerContainer.addClass('ppq-timer-warning').removeClass('ppq-timer-danger');

					// Announce five minute warning to screen readers (once)
					if (!self.fiveMinuteWarningAnnounced) {
						self.fiveMinuteWarningAnnounced = true;
						self.announceToScreenReader(
							pressprimerQuiz.strings.fiveMinuteWarning || 'Warning: Five minutes remaining!',
							'polite'
						);
					}
				}

				// Time expired - auto-submit
				if (self.timeRemaining <= 0) {
					clearInterval(self.timerInterval);
					self.handleTimeExpired();
				}
			}, 1000);
		},

		/**
		 * Handle time expiration
		 */
		handleTimeExpired: function() {
			// If the timer was actually the assignment deadline, mark it as such
			if (this.isUsingDeadlineAsTimer) {
				this.isDeadlineSubmit = true;
			}
			// Auto-submit quiz (no alert - will redirect to results with timeout message)
			this.submitQuiz(true);
		},

		/**
		 * Initialize effective timer
		 *
		 * Determines whether to use quiz time limit, assignment deadline, or both.
		 * When both exist, uses whichever expires first.
		 */
		initEffectiveTimer: function() {
			const hasTimeLimit = this.timeLimit && this.timeRemaining;
			const hasDeadline = this.deadlineTimestamp;

			if (!hasTimeLimit && !hasDeadline) {
				// No timer needed
				return;
			}

			if (hasTimeLimit && hasDeadline) {
				// Both exist - determine which expires first
				const now = Math.floor(Date.now() / 1000);
				const deadlineRemaining = this.deadlineTimestamp - now;

				if (deadlineRemaining <= 0) {
					// Deadline already passed
					if (this.autoSubmitDeadline && !this.allowLate) {
						this.handleDeadlineExpired();
						return;
					}
					// Late allowed, just use time limit
					this.startTimer();
					return;
				}

				// Use whichever is shorter
				if (deadlineRemaining < this.timeRemaining) {
					// Deadline is sooner than time limit - use deadline as effective timer
					this.timeRemaining = deadlineRemaining;
					this.isUsingDeadlineAsTimer = true;
					this.startTimer();
				} else {
					// Time limit is sooner - use time limit, but also track deadline
					this.startTimer();
					// Set up deadline checker in background (deadline might matter for late policy)
					this.startDeadlineChecker();
				}
			} else if (hasTimeLimit) {
				// Only time limit, use standard timer
				this.startTimer();
			} else if (hasDeadline) {
				// Only deadline, use deadline timer
				this.initDeadlineTimer();
			}
		},

		/**
		 * Initialize deadline timer
		 *
		 * Shows a countdown timer if within 1 hour of deadline.
		 * Auto-submits when deadline passes (if late not allowed).
		 */
		initDeadlineTimer: function() {
			if (!this.deadlineTimestamp) {
				return;
			}

			// Calculate seconds until deadline
			const now = Math.floor(Date.now() / 1000);
			this.deadlineRemaining = this.deadlineTimestamp - now;

			// If deadline already passed
			if (this.deadlineRemaining <= 0) {
				if (this.autoSubmitDeadline && !this.allowLate) {
					// Auto-submit immediately
					this.handleDeadlineExpired();
				}
				// If late allowed, just let them continue
				return;
			}

			// Only show timer if within 1 hour (3600 seconds)
			const ONE_HOUR = 3600;
			if (this.deadlineRemaining <= ONE_HOUR) {
				this.startDeadlineTimer();
			} else {
				// Schedule to start showing timer when within 1 hour
				const self = this;
				const delayMs = (this.deadlineRemaining - ONE_HOUR) * 1000;
				setTimeout(function() {
					self.deadlineRemaining = ONE_HOUR;
					self.startDeadlineTimer();
				}, delayMs);

				// Also set up a hidden interval to check for deadline
				this.startDeadlineChecker();
			}
		},

		/**
		 * Start deadline countdown timer display
		 */
		startDeadlineTimer: function() {
			const self = this;
			const $timer = $('#ppq-timer');

			// Show timer element if hidden (for quizzes without time limit)
			if (!this.timeLimit) {
				$timer.show();
			}

			// Clear any existing interval
			if (this.deadlineInterval) {
				clearInterval(this.deadlineInterval);
			}

			// Update display immediately
			this.updateDeadlineDisplay();

			// Start countdown
			this.deadlineInterval = setInterval(function() {
				self.deadlineRemaining--;

				// Update display
				self.updateDeadlineDisplay();

				// Warning announcements
				if (self.deadlineRemaining <= 300 && !self.deadlineWarningAnnounced) {
					self.deadlineWarningAnnounced = true;
					self.announceToScreenReader(
						pressprimerQuiz.strings.deadlineWarning || 'Warning: The submission deadline is approaching!',
						'polite'
					);
				}

				// Deadline reached
				if (self.deadlineRemaining <= 0) {
					clearInterval(self.deadlineInterval);
					self.handleDeadlineExpired();
				}
			}, 1000);
		},

		/**
		 * Start a hidden checker for deadline (when more than 1 hour away)
		 */
		startDeadlineChecker: function() {
			const self = this;

			// Check every minute
			this.deadlineInterval = setInterval(function() {
				const now = Math.floor(Date.now() / 1000);
				const remaining = self.deadlineTimestamp - now;

				if (remaining <= 0) {
					clearInterval(self.deadlineInterval);
					self.handleDeadlineExpired();
				}
			}, 60000);
		},

		/**
		 * Update deadline timer display
		 */
		updateDeadlineDisplay: function() {
			const $timer = $('#ppq-timer');
			const $timerContainer = $timer.closest('.ppq-timer-container');
			const minutes = Math.floor(this.deadlineRemaining / 60);
			const seconds = this.deadlineRemaining % 60;
			const timeString = this.padZero(minutes) + ':' + this.padZero(seconds);
			$timer.text(timeString);

			// Apply warning styles to both timer and container (for theme compatibility)
			if (this.deadlineRemaining <= 60) {
				$timer.removeClass('ppq-timer-warning').addClass('ppq-timer-danger');
				$timerContainer.removeClass('ppq-timer-warning').addClass('ppq-timer-danger');
			} else if (this.deadlineRemaining <= 300) {
				$timer.addClass('ppq-timer-warning').removeClass('ppq-timer-danger');
				$timerContainer.addClass('ppq-timer-warning').removeClass('ppq-timer-danger');
			}
		},

		/**
		 * Handle deadline expiration
		 */
		handleDeadlineExpired: function() {
			// If late submissions are allowed, don't auto-submit
			if (this.allowLate) {
				// User can continue, will be marked as late on server
				return;
			}

			// Mark this as a deadline-triggered submission
			this.isDeadlineSubmit = true;

			// Auto-submit quiz
			this.submitQuiz(true);
		},

		/**
		 * Submit quiz
		 *
		 * @param {boolean} autoSubmit Whether this is an auto-submit due to timeout
		 */
		submitQuiz: function(autoSubmit) {
			const self = this;
			autoSubmit = autoSubmit || false;

			// Prevent double submission
			if (this.isSubmitting) {
				return;
			}

			// Check for unanswered questions (skip for auto-submit on timeout)
			if (!autoSubmit) {
				const unanswered = this.getUnansweredQuestions();
				if (unanswered.length > 0) {
					this.showUnansweredWarning(unanswered);
					return;
				}
			}

			// Set submitting state
			this.isSubmitting = true;
			this.isAutoSubmit = autoSubmit;

			// Disable buttons and show loading
			const $submitButton = $('#ppq-submit-button');
			$submitButton.prop('disabled', true).addClass('ppq-loading');

			// Stop timer if running
			if (this.timerInterval) {
				clearInterval(this.timerInterval);
			}

			// Stop deadline timer if running
			if (this.deadlineInterval) {
				clearInterval(this.deadlineInterval);
			}

			// Wait for any pending saves to complete
			const waitForSaves = function() {
				// Check if still saving or have pending saves
				if (self.isSaving || Object.keys(self.pendingSaves).length > 0) {
					// Wait a bit and check again
					setTimeout(waitForSaves, 100);
					return;
				}

				// All saves complete - proceed with submission
				self.performSubmission();
			};

			// Start waiting
			waitForSaves();
		},

		/**
		 * Perform the actual submission
		 */
		performSubmission: function() {
			const self = this;
			const $submitButton = $('#ppq-submit-button');

			// Stop heartbeat and get final active time
			this.stopHeartbeat();
			const finalActiveElapsedMs = this.getCurrentActiveElapsedMs();

			// Make AJAX request
			$.ajax({
				url: pressprimerQuiz.ajaxUrl,
				type: 'POST',
				data: {
					action: 'pressprimer_quiz_submit_quiz',
					nonce: pressprimerQuiz.nonce,
					attempt_id: this.attemptId,
					timed_out: this.isAutoSubmit,
					deadline_submit: this.isDeadlineSubmit,
					current_url: window.location.href,
					active_elapsed_ms: finalActiveElapsedMs,
					guest_token: this.guestToken
				},
				success: (response) => {
					if (response.success && response.data.redirect_url) {
						// Redirect to results page
						window.location.href = response.data.redirect_url;
					} else {
						// Show error
						const errorMessage = response.data && response.data.message
							? response.data.message
							: pressprimerQuiz.strings.error;
						alert(errorMessage);

						// Re-enable submit button
						$submitButton.prop('disabled', false).removeClass('ppq-loading');
						self.isSubmitting = false;
						self.isDeadlineSubmit = false;

						// Restart timer if needed
						if (self.timeLimit && self.timeRemaining > 0) {
							self.startTimer();
						}

						// Restart deadline tracking if deadline hasn't passed
						if (self.deadlineTimestamp && !self.timeLimit) {
							const now = Math.floor(Date.now() / 1000);
							const remaining = self.deadlineTimestamp - now;
							if (remaining > 0) {
								self.deadlineRemaining = remaining;
								self.startDeadlineTimer();
							}
						}
					}
				},
				error: () => {
					alert(pressprimerQuiz.strings.error);
					$submitButton.prop('disabled', false).removeClass('ppq-loading');
					self.isSubmitting = false;
					self.isDeadlineSubmit = false;

					// Restart timer if needed
					if (self.timeLimit && self.timeRemaining > 0) {
						self.startTimer();
					}

					// Restart deadline tracking if deadline hasn't passed
					if (self.deadlineTimestamp && !self.timeLimit) {
						const now = Math.floor(Date.now() / 1000);
						const remaining = self.deadlineTimestamp - now;
						if (remaining > 0) {
							self.deadlineRemaining = remaining;
							self.startDeadlineTimer();
						}
					}
				}
			});
		},

		/**
		 * Pad number with leading zero
		 *
		 * @param {number} num Number to pad
		 * @return {string} Padded string
		 */
		padZero: function(num) {
			return num < 10 ? '0' + num : num.toString();
		},

		/**
		 * Start active time tracking
		 *
		 * Tracks time when the user is actively engaged with the quiz.
		 * Pauses when tab is hidden or browser is minimized.
		 */
		startActiveTimeTracking: function() {
			// Initialize timestamp
			this.lastActiveTimestamp = Date.now();
			this.isTabActive = true;

			// Start heartbeat interval for syncing time when no answers are being saved
			this.startHeartbeat();
		},

		/**
		 * Pause active time tracking
		 *
		 * Called when tab becomes hidden or browser loses focus.
		 */
		pauseActiveTimeTracking: function() {
			if (!this.isTabActive) {
				return; // Already paused
			}

			// Calculate elapsed time since last timestamp
			if (this.lastActiveTimestamp) {
				const now = Date.now();
				this.activeElapsedMs += (now - this.lastActiveTimestamp);
			}

			this.isTabActive = false;
			this.lastActiveTimestamp = null;
		},

		/**
		 * Resume active time tracking
		 *
		 * Called when tab becomes visible again.
		 */
		resumeActiveTimeTracking: function() {
			if (this.isTabActive) {
				return; // Already active
			}

			this.isTabActive = true;
			this.lastActiveTimestamp = Date.now();
		},

		/**
		 * Get current total active elapsed time in milliseconds
		 *
		 * @return {number} Total active time in milliseconds
		 */
		getCurrentActiveElapsedMs: function() {
			let total = this.activeElapsedMs;

			// Add time since last timestamp if currently active
			if (this.isTabActive && this.lastActiveTimestamp) {
				total += (Date.now() - this.lastActiveTimestamp);
			}

			return Math.round(total);
		},

		/**
		 * Start heartbeat interval for syncing time
		 *
		 * Syncs active time every 30 seconds when no auto-save is occurring.
		 */
		startHeartbeat: function() {
			const self = this;

			// Clear any existing interval
			if (this.heartbeatInterval) {
				clearInterval(this.heartbeatInterval);
			}

			// Start heartbeat
			this.heartbeatInterval = setInterval(function() {
				// Only sync if tab is active and not currently saving
				if (self.isTabActive && !self.isSaving && Object.keys(self.pendingSaves).length === 0) {
					self.syncActiveTime();
				}
			}, this.heartbeatDelay);
		},

		/**
		 * Stop heartbeat interval
		 */
		stopHeartbeat: function() {
			if (this.heartbeatInterval) {
				clearInterval(this.heartbeatInterval);
				this.heartbeatInterval = null;
			}
		},

		/**
		 * Get list of unanswered question indices
		 *
		 * @return {Array} Array of question indices (0-based) that are unanswered
		 */
		getUnansweredQuestions: function() {
			const unanswered = [];

			$('.ppq-question').each(function(index) {
				const $question = $(this);
				const hasAnswer = $question.find('.ppq-answer-input:checked').length > 0;

				if (!hasAnswer) {
					unanswered.push(index);
				}
			});

			return unanswered;
		},

		/**
		 * Show warning about unanswered questions
		 *
		 * @param {Array} unanswered Array of unanswered question indices
		 */
		showUnansweredWarning: function(unanswered) {
			const self = this;
			const questionNumbers = unanswered.map(function(idx) { return idx + 1; });

			// Build message
			let message;
			if (unanswered.length === 1) {
				message = pressprimerQuiz.strings.unansweredSingle.replace('{question}', questionNumbers[0]);
			} else if (unanswered.length <= 5) {
				message = pressprimerQuiz.strings.unansweredMultiple.replace('{questions}', questionNumbers.join(', '));
			} else {
				message = pressprimerQuiz.strings.unansweredMany.replace('{count}', unanswered.length);
			}

			// Remove any existing warning
			$('.ppq-unanswered-overlay').remove();

			// Create overlay with warning
			const $overlay = $('<div class="ppq-unanswered-overlay">' +
				'<div class="ppq-unanswered-warning">' +
				'<p><strong>' + pressprimerQuiz.strings.unansweredTitle + '</strong></p>' +
				'<p>' + message + '</p>' +
				'<div class="ppq-unanswered-actions">' +
				'<button type="button" class="ppq-button ppq-button-secondary ppq-go-to-first">' +
				pressprimerQuiz.strings.goToQuestion.replace('{question}', questionNumbers[0]) +
				'</button>' +
				'<button type="button" class="ppq-button ppq-button-primary ppq-submit-anyway">' +
				pressprimerQuiz.strings.submitAnyway +
				'</button>' +
				'</div>' +
				'</div>' +
				'</div>');

			// Append to body
			$('body').append($overlay);

			// Bind events
			$overlay.find('.ppq-go-to-first').on('click', function() {
				$overlay.remove();
				self.showQuestion(unanswered[0]);
			});

			$overlay.find('.ppq-submit-anyway').on('click', function() {
				$overlay.remove();
				// Force submit without checking again
				self.isSubmitting = true;
				self.isAutoSubmit = false;
				$('#ppq-submit-button').prop('disabled', true).addClass('ppq-loading');

				// Stop timer if running
				if (self.timerInterval) {
					clearInterval(self.timerInterval);
				}

				// Wait for pending saves then submit
				const waitForSaves = function() {
					if (self.isSaving || Object.keys(self.pendingSaves).length > 0) {
						setTimeout(waitForSaves, 100);
						return;
					}
					self.performSubmission();
				};
				waitForSaves();
			});

			// Close overlay when clicking backdrop
			$overlay.on('click', function(e) {
				if ($(e.target).hasClass('ppq-unanswered-overlay')) {
					$overlay.remove();
				}
			});
		},

		/**
		 * Show message when user tries to go back but backward navigation is not allowed
		 */
		showBackwardNotAllowedMessage: function() {
			const $currentQuestion = $('.ppq-question[data-question-index="' + this.currentQuestionIndex + '"]');

			// Remove any existing message
			$('.ppq-backward-not-allowed-message').remove();

			// Create message element
			const $message = $('<div class="ppq-backward-not-allowed-message ppq-notice ppq-notice-warning">' +
				'<p>' + (pressprimerQuiz.strings.backwardNotAllowed || 'You cannot go back to previous questions in this quiz.') + '</p>' +
				'</div>');

			// Insert message before the question content
			$currentQuestion.find('.ppq-question-content').before($message);

			// Scroll to make message visible
			$message[0].scrollIntoView({ behavior: 'smooth', block: 'center' });

			// Announce to screen reader
			this.announceToScreenReader(
				pressprimerQuiz.strings.backwardNotAllowed || 'You cannot go back to previous questions in this quiz.',
				'assertive'
			);

			// Auto-remove after 5 seconds
			setTimeout(function() {
				$message.slideUp(200, function() {
					$(this).remove();
				});
			}, 5000);
		},

		/**
		 * Sync active time to server
		 *
		 * Lightweight endpoint that only updates active_elapsed_ms.
		 */
		syncActiveTime: function() {
			// Don't sync if offline
			if (!this.isOnline) {
				return;
			}

			const activeElapsedMs = this.getCurrentActiveElapsedMs();

			$.ajax({
				url: pressprimerQuiz.ajaxUrl,
				type: 'POST',
				data: {
					action: 'pressprimer_quiz_sync_time',
					nonce: pressprimerQuiz.nonce,
					attempt_id: this.attemptId,
					active_elapsed_ms: activeElapsedMs,
					guest_token: this.guestToken
				},
				// Silent - no UI feedback for heartbeat
				success: function() {
					// Time synced successfully
				},
				error: function() {
					// Ignore errors for heartbeat - will retry next interval
				}
			});
		},

		/**
		 * Sync active time using Beacon API (for tab hidden/page unload)
		 *
		 * Uses navigator.sendBeacon which is more reliable when the page
		 * is being hidden or unloaded, especially in Safari.
		 */
		syncActiveTimeBeacon: function() {
			// Don't sync if offline
			if (!this.isOnline) {
				return;
			}

			// Check if sendBeacon is supported
			if (!navigator.sendBeacon) {
				// Fall back to regular AJAX
				this.syncActiveTime();
				return;
			}

			const activeElapsedMs = this.getCurrentActiveElapsedMs();

			// Build form data for beacon
			const formData = new FormData();
			formData.append('action', 'pressprimer_quiz_sync_time');
			formData.append('nonce', pressprimerQuiz.nonce);
			formData.append('attempt_id', this.attemptId);
			formData.append('active_elapsed_ms', activeElapsedMs);
			if (this.guestToken) {
				formData.append('guest_token', this.guestToken);
			}

			// Send using beacon API - fire and forget, works reliably when tab is hidden
			navigator.sendBeacon(pressprimerQuiz.ajaxUrl, formData);
		}
	};

	/**
	 * Initialize on document ready
	 */
	$(document).ready(function() {
		QuizLanding.init();
		QuizInterface.init();
	});

})(jQuery);
