"""
Quiz and assessment related routes
"""
import json
import uuid
import logging
import mysql.connector
import asyncio
import time
import sys
import os
from datetime import datetime
from flask import Blueprint, request, jsonify
from utils import (require_api_key, encrypt_response, format_api_response, 
                   create_error_response, get_db_connection)

# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Import AI generation capabilities
from services.ai_provider import ai_generate_async
from services.ai_config import is_groq_enabled

try:
    import g4f
    g4f_available = True
except ImportError:
    g4f_available = False

# AI is available if either Groq or g4f is configured
ai_available = g4f_available or is_groq_enabled()

# Import dynamic AI model configuration
from aimodel_selector.models_config import get_default_model, get_model_config, get_all_models

logger = logging.getLogger(__name__)
quiz_bp = Blueprint('quiz', __name__)

import re

def extract_content_from_response(response):
    """
    Extract the actual text content from AI response.
    g4f returns different formats depending on provider:
    - String: direct text content
    - Dict: {'choices': [{'message': {'content': '...'}}], ...}
    - String repr of dict: "{'choices': [...]}"
    """
    if not response:
        return ""
    
    # Log response type for debugging
    logger.debug(f"Response type: {type(response).__name__}")
    
    # If response is a dict (common g4f format) - CHECK THIS FIRST
    if isinstance(response, dict):
        logger.debug(f"Response is dict, keys: {list(response.keys())[:5]}")
        # Format: {'choices': [{'message': {'content': '...'}}]}
        if 'choices' in response:
            choices = response.get('choices', [])
            logger.debug(f"Choices type: {type(choices).__name__}, length: {len(choices) if choices else 0}")
            if choices and len(choices) > 0:
                first_choice = choices[0]
                logger.debug(f"First choice type: {type(first_choice).__name__}")
                if isinstance(first_choice, dict):
                    message = first_choice.get('message', {})
                    logger.debug(f"Message type: {type(message).__name__}")
                    if isinstance(message, dict):
                        content = message.get('content', '')
                        logger.debug(f"Content extracted, length: {len(content) if content else 0}")
                        if content:
                            return content
        # Fallback format: {'content': '...'} or {'message': {'content': '...'}}
        if 'content' in response:
            return response['content']
        if 'message' in response and isinstance(response['message'], dict):
            return response['message'].get('content', '')
        # If dict but no content found, log and fall through
        logger.warning(f"Dict response but no content found. Keys: {list(response.keys())}")
    
    # If it's a string
    if isinstance(response, str):
        stripped = response.strip()
        
        # Check if it's a string representation of a dict (starts with { and contains 'choices')
        if stripped.startswith('{') and 'choices' in stripped:
            try:
                import ast
                # Try ast.literal_eval for Python dict format
                parsed = ast.literal_eval(stripped)
                return extract_content_from_response(parsed)  # Recursively extract
            except:
                try:
                    # Try json.loads for JSON format
                    parsed = json.loads(stripped)
                    return extract_content_from_response(parsed)
                except:
                    pass
        
        # It's a regular string content, return as is
        return stripped
    
    # If response is a list (some providers)
    if isinstance(response, list) and len(response) > 0:
        first_item = response[0]
        if isinstance(first_item, dict):
            return first_item.get('message', {}).get('content', '')
    
    # Last resort: convert to string
    logger.warning(f"Falling back to str(response) for type: {type(response).__name__}")
    return str(response)

def _fix_invalid_json_escapes(text):
    """
    Fix invalid JSON escape sequences such as LaTeX \\m, \\a, \\( etc.
    Valid JSON escapes after backslash: " \\ / b f n r t u
    Any other \\X is invalid and gets the backslash escaped to \\\\X.
    Uses re.sub with \\(.) so each \\X pair is consumed atomically —
    valid pairs (e.g. \\\\m, \\n) pass through unchanged.
    """
    VALID = {'"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u'}
    def _fix(m):
        s = m.group(0)
        return s if s[1] in VALID else '\\\\' + s[1]
    return re.sub(r'\\(.)', _fix, text, flags=re.DOTALL)


def parse_ai_json(text):
    """
    Parse AI JSON response with multi-stage error recovery.
    1. Try direct parse (handles well-formed JSON from Groq etc.)
    2. Fix invalid JSON escape sequences (LaTeX math like \\mr^2)
    3. Fallback: convert literal \\n sequences (legacy g4f models)
    """
    if not text:
        raise ValueError("Empty text")

    text = text.strip()

    # Remove markdown code blocks if present
    if text.startswith('```'):
        text = re.sub(r'^```(?:json)?\s*', '', text)
        text = re.sub(r'\s*```$', '', text)
        text = text.strip()

    # Stage 1: direct parse
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass

    # Stage 2: fix invalid escape sequences (e.g. LaTeX \m inside JSON strings)
    try:
        return json.loads(_fix_invalid_json_escapes(text))
    except json.JSONDecodeError:
        pass

    # Stage 3: legacy fallback — some older models output literal \n as 2 chars
    text = text.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
    return json.loads(text)

def safe_log_unicode(text, max_length=800):
    """Safely log Unicode text without encoding errors"""
    try:
        if isinstance(text, str):
            return text[:max_length].encode('utf-8', errors='replace').decode('utf-8')
        return str(text)[:max_length]
    except Exception:
        return f"[Logging Error: Could not safely encode text of length {len(str(text)) if text else 0}]"

def get_available_ai_models():
    """Get list of available AI models for frontend - filtered by active backend"""
    try:
        models = get_all_models()
        try:
            from services.ai_config import is_groq_enabled, is_g4f_enabled
            if is_groq_enabled():
                models = [m for m in models if m.get('backend_type') == 'groq']
            elif is_g4f_enabled():
                models = [m for m in models if m.get('backend_type') == 'g4f']
        except ImportError:
            pass
        # key = model identifier for API calls, name = display name
        return [{'key': model['name'], 'name': model['description']} for model in models]
    except Exception as e:
        logger.error(f"Error fetching models: {e}")
        # Fallback to default model
        return [{'key': 'geminisearch', 'name': 'Gemini Search (Default)'}]

def get_dynamic_model_config(model_name=None, user_id=None):
    """Get model configuration dynamically from database"""
    try:
        if not model_name:
            model_name = get_default_model(user_id=user_id)
        config = get_model_config(model_name)
        return {
            'model': config.get('model', 'geminisearch'),
            'name': config.get('description', 'AI Model'),
            'provider': config.get('provider', 'PollinationsAI'),
            'timeout': 60
        }
    except Exception as e:
        logger.error(f"Error getting model config: {e}")
        # Fallback
        return {
            'model': 'geminisearch',
            'name': 'Gemini Search',
            'provider': 'PollinationsAI',
            'timeout': 60
        }

async def generate_ai_content_with_retry(model_key, messages, max_retries=5, user_id=None):
    """Generate AI content with retry mechanism and exponential backoff - now with dynamic models"""
    try:
        model_config = get_dynamic_model_config(model_key, user_id=user_id)
    except Exception:
        # Use default if specific model not found
        model_config = get_dynamic_model_config(user_id=user_id)
    
    for attempt in range(max_retries):
        try:
            logger.info(f"AI generation attempt {attempt + 1}/{max_retries} with model {model_key}")
            
            # Only pass provider if configured; some models infer provider internally
            create_kwargs = {
                'model': model_config['model'],
                'messages': messages,
            }

            result = await ai_generate_async(
                messages=messages,
                model=model_config['model'],
            )
            response = result.content
            
            if response and response.strip():
                logger.info(f"Successfully generated content with {model_key} on attempt {attempt + 1}")
                return response
            else:
                raise Exception("Empty response from AI")
                
        except Exception as e:
            logger.warning(f"AI generation attempt {attempt + 1} failed with {model_key}: {e}")
            
            if attempt < max_retries - 1:
                # Exponential backoff: 2^attempt seconds
                wait_time = 2 ** attempt
                logger.info(f"Waiting {wait_time} seconds before retry...")
                await asyncio.sleep(wait_time)
            else:
                # Last attempt failed, try fallback to default model
                try:
                    default_model = get_default_model(user_id=user_id)
                    if model_key != default_model:
                        logger.info(f"Falling back to default model '{default_model}' after {model_key} failed")
                        return await generate_ai_content_with_retry(default_model, messages, user_id=user_id, max_retries=2)
                except Exception:
                    pass
                raise Exception(f"All attempts failed for model {model_key}: {str(e)}")
    
    raise Exception(f"Failed to generate content after {max_retries} attempts")

async def generate_and_parse_ai_quiz_with_retry(model_key, messages, max_retries=5, user_id=None):
    """Generate AI quiz content and parse JSON with retry mechanism and robust cleaning"""
    try:
        model_config = get_dynamic_model_config(model_key, user_id=user_id)
    except Exception:
        model_config = get_dynamic_model_config(user_id=user_id)
    
    for attempt in range(max_retries):
        try:
            logger.info(f"AI quiz generation attempt {attempt + 1}/{max_retries} with model {model_key}")
            
            create_kwargs = {
                'model': model_config['model'],
                'messages': messages,
            }
                
            result = await ai_generate_async(
                messages=messages,
                model=model_config['model'],
            )
            response = result.content
            
            if not response:
                raise Exception("Empty response from AI")
            
            # --- SIMPLE PARSING ---
            
            # 1. Extract actual content (handles raw API response objects)
            content = extract_content_from_response(response)
            if not content:
                raise Exception("Could not extract content from AI response")
            
            logger.debug(f"Extracted content length: {len(content)} chars")
            
            # 2. Parse directly - AI should output valid JSON
            try:
                ai_questions = parse_ai_json(content)
            except json.JSONDecodeError as e:
                logger.error(f"JSON Parse Error: {e.msg} at position {e.pos}")
                logger.debug(f"Content start: {content[:300]}")
                raise e

            # Validation
            if not isinstance(ai_questions, list) or len(ai_questions) == 0:
                raise ValueError("Parsed JSON is not a list or is empty")
            
            if not ai_questions[0].get('question_text'):
                raise ValueError("JSON structure incorrect: missing 'question_text'")

            logger.info(f"Successfully generated and parsed {len(ai_questions)} questions with {model_key}")
            return ai_questions

        except (json.JSONDecodeError, ValueError) as e:
            logger.warning(f"JSON parsing failed on attempt {attempt + 1}: {str(e)[:100]}")
            
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt
                # If parsing failed, we can optionally append a correction message to the prompt for the next try
                # But for now, we just wait and retry (or switch models in the outer loop if implemented)
                await asyncio.sleep(wait_time)
            else:
                # Fallback logic logic (same as your original)
                try:
                    default_model = get_default_model(user_id=user_id)
                    if model_key != default_model:
                        logger.info(f"Falling back to default model '{default_model}'")
                        return await generate_and_parse_ai_quiz_with_retry(default_model, messages, user_id=user_id, max_retries=2)
                except Exception:
                    pass
                raise Exception(f"Final JSON parsing failure: {str(e)}")
        
        except Exception as e:
            # Handle network/timeout errors
            logger.warning(f"AI generation error on attempt {attempt + 1}: {e}")
            if attempt < max_retries - 1:
                await asyncio.sleep(2 ** attempt)
            else:
                 raise Exception(f"All attempts failed: {str(e)}")
                 
    raise Exception("Max retries reached")

@quiz_bp.route('/api/ai-models', methods=['GET'])
@require_api_key
@encrypt_response
def get_ai_models():
    """Get available AI models for quiz generation"""
    try:
        if not ai_available:
            return create_error_response("AI service is not available", 503)
        
        models = get_available_ai_models()
        models_data = {'models': models}
        return format_api_response(
            data=models_data,
            message="AI models retrieved successfully",
            include_legacy_fields=models_data
        )
    except Exception as e:
        logger.error(f"Error getting AI models: {str(e)}")
        return create_error_response(f"Failed to get AI models: {str(e)}")


@quiz_bp.route('/api/quiz-question/<question_id>', methods=['GET'])
@require_api_key
@encrypt_response
def get_quiz_question(question_id):
    """Get individual quiz question details by ID"""
    conn = None
    cursor = None
    try:
        # Get user_id from query params for access verification
        user_id = request.args.get('user_id')
        
        if not user_id:
            return create_error_response("User ID is required", 400)
        
        from database import get_cached_connection
        conn = get_cached_connection()
        if not conn:
            return create_error_response("Database connection failed", 500)
        
        # Ensure UTF-8 encoding
        cursor = conn.cursor(dictionary=True, buffered=True)
        cursor.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
        cursor.execute("SET character_set_client = utf8mb4")
        cursor.execute("SET character_set_results = utf8mb4")
        cursor.execute("SET character_set_connection = utf8mb4")
        cursor.execute("SET collation_connection = utf8mb4_unicode_ci")
        
        # Get question with access verification (user must be enrolled in the course)
        cursor.execute("""
            SELECT 
                q.id as question_id,
                q.question_text,
                q.options,
                q.correct_answer_index,
                q.difficulty,
                q.explanation,
                q.marks,
                q.is_ai_generated,
                q.ai_model,
                q.user_id as created_by_user_id,
                q.created_at,
                t.id as topic_id,
                t.title as topic_title,
                t.parent_topic_id,
                t.level,
                t.full_path as topic_path,
                c.id as course_id,
                c.title as course_title
            FROM questions q
            JOIN topics t ON q.topic_id = t.id
            JOIN courses c ON t.course_id = c.id
            LEFT JOIN enrollments e ON c.id = e.course_id AND e.user_id = %s AND e.payment_status IN ('free', 'paid')
            WHERE q.id = %s AND (e.user_id IS NOT NULL OR q.user_id = %s)
        """, [user_id, question_id, user_id])
        
        question = cursor.fetchone()
        
        if not question:
            return create_error_response("Question not found or access denied", 404)
        
        # Parse options
        formatted_options = []
        try:
            if question['options']:
                if isinstance(question['options'], str):
                    parsed_options = json.loads(question['options'])
                else:
                    parsed_options = question['options']
                
                if isinstance(parsed_options, list):
                    correct_index = int(question['correct_answer_index'])
                    
                    for i, option_text in enumerate(parsed_options):
                        formatted_options.append({
                            'option_id': str(i),
                            'option_text': str(option_text).strip(),
                            'is_correct': i == correct_index,
                            'order': i + 1
                        })
        except Exception as e:
            logger.warning(f"Error parsing options for question {question_id}: {e}")
            # Fallback options
            for i in range(4):
                formatted_options.append({
                    'option_id': str(i),
                    'option_text': f'Option {i + 1}',
                    'is_correct': i == 0,
                    'order': i + 1
                })
        
        # Format response
        question_data = {
            'question_id': str(question['question_id']),
            'question_text': question['question_text'],
            'options': formatted_options,
            'difficulty': question['difficulty'],
            'explanation': question.get('explanation', ''),
            'marks': question.get('marks', 1),
            'topic_id': question['topic_id'],
            'topic_title': question['topic_title'],
            'parent_topic_id': question.get('parent_topic_id'),
            'topic_level': question.get('level', 0),
            'topic_path': question.get('topic_path'),
            'course_id': question['course_id'],
            'course_title': question['course_title'],
            'is_ai_generated': bool(question.get('is_ai_generated', False)),
            'ai_model': question.get('ai_model'),
            'created_at': question['created_at'].isoformat() if question.get('created_at') else None
        }
        
        return format_api_response(
            data=question_data,
            message="Question retrieved successfully"
        )
        
    except Exception as e:
        logger.error(f"Error in get_quiz_question: {str(e)}")
        return create_error_response(f"Failed to retrieve question: {str(e)}")
    finally:
        if cursor:
            cursor.close()
        if conn:
            try:
                conn.close()
            except Exception:
                pass


@quiz_bp.route('/api/random-quiz', methods=['POST'])
@require_api_key
@encrypt_response
def generate_random_quiz():
    """Generate a random quiz from selected topics"""
    conn = None
    cursor = None
    try:
        data = request.get_json() or {}
        user_id = data.get('user_id')
        topic_ids = data.get('topic_ids', [])
        question_count = min(int(data.get('question_count', 10)), 50)  # Max 50 questions
        difficulty_level = data.get('difficulty_level', 'all')  # all, easy, medium, hard
        negative_marking = data.get('negative_marking', False)
        negative_marking_percentage = data.get('negative_marking_percentage', 25)
        use_user_questions = data.get('use_user_questions', False)  # Use user's AI questions
        ai_only = data.get('ai_only', False)  # Use only user's AI-generated questions
        
        if not user_id:
            return create_error_response("User ID is required", 400)
        
        if not topic_ids:
            return create_error_response("At least one topic must be selected", 400)
        
        # Use optimized connection with caching for better performance
        from database import get_cached_connection
        conn = get_cached_connection()
        if not conn:
            return create_error_response("Database connection failed", 500)
        
        # Ensure UTF-8 encoding for proper Nepali/Devanagari text and LaTeX storage
        cursor = conn.cursor(dictionary=True, buffered=True)
        cursor.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
        cursor.execute("SET character_set_client = utf8mb4")
        cursor.execute("SET character_set_results = utf8mb4")
        cursor.execute("SET character_set_connection = utf8mb4")
        cursor.execute("SET collation_connection = utf8mb4_unicode_ci")
        
        # Verify user has access to these topics (including hierarchical structure)
        placeholders = ','.join(['%s'] * len(topic_ids))
        cursor.execute(f"""
            SELECT t.id, t.parent_topic_id, t.level, t.full_path
            FROM topics t
            JOIN courses c ON t.course_id = c.id
            JOIN enrollments e ON c.id = e.course_id AND e.user_id = %s AND e.payment_status IN ('free', 'paid')
            WHERE t.id IN ({placeholders})
        """, [user_id] + topic_ids)
        
        accessible_topics = [row['id'] for row in cursor.fetchall()]
        
        if not accessible_topics:
            return create_error_response("No accessible topics found", 403)
        
        # Build difficulty filter
        difficulty_filter = ""
        difficulty_params = []
        if difficulty_level != 'all':
            difficulty_filter = "AND q.difficulty = %s"
            difficulty_params = [difficulty_level]
        
        # Build user question filter
        user_filter = ""
        user_params = []
        if ai_only:
            # Only user's AI-generated questions
            user_filter = "AND q.user_id = %s AND q.is_ai_generated = 1"
            user_params = [user_id]
        elif use_user_questions:
            # Mix of user's AI questions and system/topic questions
            user_filter = "AND (q.user_id = %s OR q.user_id IS NULL)"
            user_params = [user_id]
        else:
            # Only system/topic questions (no user-generated questions)
            user_filter = "AND q.user_id IS NULL"
        
        # Get random questions from the accessible topics
        placeholders = ','.join(['%s'] * len(accessible_topics))
        cursor.execute(f"""
            SELECT 
                q.id as question_id,
                q.question_text,
                q.options,
                q.correct_answer_index,
                q.difficulty,
                q.explanation,
                q.marks,
                q.is_ai_generated,
                q.ai_model,
                t.title as topic_title,
                t.id as topic_id,
                t.parent_topic_id,
                t.level,
                t.full_path,
                c.title as course_title
            FROM questions q
            JOIN topics t ON q.topic_id = t.id
            JOIN courses c ON t.course_id = c.id
            WHERE q.topic_id IN ({placeholders}) {difficulty_filter} {user_filter}
            ORDER BY RAND()
            LIMIT %s
        """, accessible_topics + difficulty_params + user_params + [question_count])
        
        questions = cursor.fetchall()
        
        if not questions:
            return create_error_response("No questions found for selected topics", 404)
        
        # Format questions for frontend
        formatted_questions = []
        for q in questions:
            # Parse options using the same robust method as analytics routes
            formatted_options = []
            try:
                # Handle options parsing with better error handling
                if q['options']:
                    if isinstance(q['options'], str):
                        parsed_options = json.loads(q['options'])
                    else:
                        parsed_options = q['options']
                    
                    # Transform options array into proper structure for frontend
                    if isinstance(parsed_options, list):
                        correct_index = int(q['correct_answer_index'])
                        
                        for i, option_text in enumerate(parsed_options):
                            option_id = str(i)  # Use index as option_id
                            is_correct = i == correct_index  # Use correct_answer_index from questions table
                            
                            formatted_options.append({
                                'option_id': option_id,
                                'option_text': str(option_text).strip(),  # Ensure it's a string and strip whitespace
                                'is_correct': is_correct
                            })
                    else:
                        # If not a list, create fallback options
                        raise ValueError("Options is not a list")
                else:
                    # No options data available
                    raise ValueError("No options data")
                    
            except (json.JSONDecodeError, TypeError, ValueError, KeyError) as e:
                logger.warning(f"Error processing options for question {q.get('question_id', 'unknown')}: {e}")
                # Fallback options with proper structure
                for i in range(4):
                    formatted_options.append({
                        'option_id': str(i),
                        'option_text': f'Option {i + 1}',
                        'is_correct': i == 0  # Default first option as correct
                    })
            
            formatted_questions.append({
                'question_id': str(q['question_id']),
                'question_text': q['question_text'],
                'options': formatted_options,
                'difficulty': q['difficulty'],
                'explanation': q.get('explanation', ''),
                'topic_title': q['topic_title'],
                'topic_id': q['topic_id'],
                'parent_topic_id': q.get('parent_topic_id'),
                'topic_level': q.get('level', 0),
                'topic_full_path': q.get('full_path'),
                'course_title': q['course_title'],
                'marks': q.get('marks', 1)  # Default to 1 mark if not specified
            })
        
        # Generate quiz metadata
        quiz_data = {
            'quiz_id': str(uuid.uuid4()),
            'title': f"Random Quiz - {len(accessible_topics)} Topics",
            'description': f"Practice quiz with {len(formatted_questions)} questions from selected topics",
            'questions': formatted_questions,
            'total_questions': len(formatted_questions),
            'time_limit': len(formatted_questions) * 60,  # 1 minute per question
            'difficulty_level': difficulty_level,
            'topics_covered': accessible_topics,
            'created_at': datetime.now().isoformat(),
            'negative_marking': negative_marking,
            'negative_marking_percentage': negative_marking_percentage if negative_marking else 0
        }
        
        return format_api_response(quiz_data)
            
    except Exception as e:
        logger.error(f"Error in generate_random_quiz: {str(e)}")
        return create_error_response(f"Failed to generate quiz: {str(e)}")
    finally:
        if cursor:
            cursor.close()
        if conn:
            try:
                conn.close()
            except Exception:
                pass


@quiz_bp.route('/api/random-quiz/submit', methods=['POST'])
@require_api_key
@encrypt_response
def submit_random_quiz():
    """Submit random quiz results and calculate score"""
    conn = None
    cursor = None
    try:
        data = request.get_json() or {}
        user_id = data.get('user_id')
        quiz_id = data.get('quiz_id')
        answers = data.get('answers', {})  # {question_id: selected_option_id}
        topic_ids = data.get('topic_ids', [])
        total_questions = data.get('total_questions', len(answers))  # Use frontend-provided total or fallback to answered count
        quiz_question_ids = data.get('quiz_question_ids', [])  # Actual question IDs from the quiz
        negative_marking = data.get('negative_marking', False)
        negative_marking_percentage = data.get('negative_marking_percentage', 25)
        
        if not all([user_id, quiz_id, answers]):
            return create_error_response("Missing required fields", 400)
        
        # Log the input data for debugging
        logger.info(f"Quiz submission - User: {user_id}, Quiz: {quiz_id}, Answers: {len(answers)}, Total Questions: {total_questions}, Quiz Question IDs: {len(quiz_question_ids) if quiz_question_ids else 'None'}")
        
        from database import get_cached_connection
        conn = get_cached_connection()
        if not conn:
            return create_error_response("Database connection failed", 500)
        
        # Ensure UTF-8 encoding for proper Nepali/Devanagari text and LaTeX storage
        cursor = conn.cursor(dictionary=True, buffered=True)
        cursor.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
        cursor.execute("SET character_set_client = utf8mb4")
        cursor.execute("SET character_set_results = utf8mb4")
        cursor.execute("SET character_set_connection = utf8mb4")
        cursor.execute("SET collation_connection = utf8mb4_unicode_ci")
        
        try:
            # Get the original questions to verify answers
            # Prioritize using actual quiz question IDs for accurate marks calculation
            if quiz_question_ids:
                # Use the actual question IDs from the quiz for precise calculation
                placeholders = ','.join(['%s'] * len(quiz_question_ids))
                cursor.execute(f"""
                    SELECT 
                        q.id as question_id,
                        q.correct_answer_index,
                        q.difficulty,
                        q.marks,
                        t.id as topic_id
                    FROM questions q
                    JOIN topics t ON q.topic_id = t.id
                    WHERE q.id IN ({placeholders})
                """, quiz_question_ids)
            elif topic_ids:
                placeholders = ','.join(['%s'] * len(topic_ids))
                cursor.execute(f"""
                    SELECT 
                        q.id as question_id,
                        q.correct_answer_index,
                        q.difficulty,
                        q.marks,
                        t.id as topic_id
                    FROM questions q
                    JOIN topics t ON q.topic_id = t.id
                    WHERE q.topic_id IN ({placeholders})
                """, topic_ids)
            else:
                # Fallback: get questions by IDs if neither quiz_question_ids nor topic_ids provided
                question_ids = list(answers.keys())
                if question_ids:
                    placeholders = ','.join(['%s'] * len(question_ids))
                    cursor.execute(f"""
                        SELECT 
                            q.id as question_id,
                            q.correct_answer_index,
                            q.difficulty,
                            q.marks,
                            t.id as topic_id
                        FROM questions q
                        JOIN topics t ON q.topic_id = t.id
                        WHERE q.id IN ({placeholders})
                    """, question_ids)
                else:
                    return create_error_response("No questions to verify", 400)
            
            questions = cursor.fetchall()
            questions_dict = {str(q['question_id']): q for q in questions}
            
            # Calculate total marks possible from the actual quiz questions
            if quiz_question_ids:
                # When using quiz_question_ids, we have the exact questions from the quiz
                total_marks_possible = sum(q.get('marks', 1) for q in questions)
                logger.info(f"Using quiz question IDs for marks calculation: {len(questions)} questions, {total_marks_possible} total marks")
            else:
                # When using topic_ids or answer_ids, calculate based on total_questions with default marks
                # This ensures we don't use marks from ALL topic questions when quiz only has a subset
                default_marks_per_question = 1
                total_marks_possible = total_questions * default_marks_per_question
                logger.info(f"Using default marks calculation: {total_questions} questions × {default_marks_per_question} = {total_marks_possible} total marks")
            
            # Calculate score with negative marking support
            # Use total_questions from frontend (actual quiz question count)
            correct_answers = 0
            wrong_answers = 0
            total_marks_obtained = 0
            topic_performance = {}
            
            # Initialize topic performance for all topics
            for question in questions:
                topic_id = question['topic_id']
                if topic_id not in topic_performance:
                    topic_performance[topic_id] = {'correct': 0, 'total': 0}
                topic_performance[topic_id]['total'] += 1
            
            for question_id, selected_option in answers.items():
                if question_id in questions_dict:
                    question = questions_dict[question_id]
                    topic_id = question['topic_id']
                    question_marks = question.get('marks', 1)
                    
                    if int(selected_option) == int(question['correct_answer_index']):
                        correct_answers += 1
                        total_marks_obtained += question_marks
                        topic_performance[topic_id]['correct'] += 1
                    else:
                        if negative_marking:
                            # Calculate negative marks for wrong answers
                            negative_marks = question_marks * (negative_marking_percentage / 100)
                            total_marks_obtained -= negative_marks
            
            # Calculate wrong answers as total questions minus correct answers
            # This includes both answered incorrectly and unanswered questions
            wrong_answers = total_questions - correct_answers
            
            # Ensure marks don't go below zero
            total_marks_obtained = max(0, total_marks_obtained)
            
            # Calculate percentage based on marks if available, otherwise use questions
            if total_marks_possible > 0:
                score_percentage = (total_marks_obtained / total_marks_possible * 100)
                logger.info(f"Score calculation: {total_marks_obtained}/{total_marks_possible} = {score_percentage:.2f}%")
            else:
                score_percentage = (correct_answers / total_questions * 100) if total_questions > 0 else 0
                logger.info(f"Fallback score calculation: {correct_answers}/{total_questions} = {score_percentage:.2f}%")
            
            # Store quiz attempt (optional - you can create a table for this)
            # For now, we'll just return the results
            
            result = {
                'quiz_id': quiz_id,
                'score': score_percentage,
                'correct_answers': correct_answers,
                'wrong_answers': wrong_answers,
                'total_questions': total_questions,
                'total_marks_obtained': total_marks_obtained,
                'total_marks_possible': total_marks_possible,
                'negative_marking': negative_marking,
                'negative_marking_percentage': negative_marking_percentage,
                'topic_performance': topic_performance,
                'completed_at': datetime.now().isoformat()
            }
            
            
            return format_api_response(
                data=result,
                message="Quiz submitted successfully",
                include_legacy_fields=result
            )
        
        except Exception as e:
            logger.error(f"Error in submit_random_quiz: {str(e)}")
            return create_error_response(f"Failed to submit quiz: {str(e)}")
    finally:
        if cursor:
            cursor.close()
        if conn:
            try:
                conn.close()
            except Exception:
                pass


@quiz_bp.route('/api/quiz-analytics', methods=['POST'])
@quiz_bp.route('/api/quiz-progress/store', methods=['POST'])
@require_api_key
@encrypt_response
def store_quiz_progress():
    """
    Store quiz progress and results for analytics
    Enhanced with better error handling and logging
    Accessible via both /api/quiz-analytics and /api/quiz-progress/store
    """
    
    try:
        data = request.get_json()
        if not data:
            logger.error("No data provided in quiz progress request")
            return create_error_response("No data provided", 400)

        # Enhanced validation
        required_fields = ['user_id', 'quiz_type', 'total_questions', 'correct_answers', 'score_percentage']
        for field in required_fields:
            if field not in data:
                logger.error(f"Missing required field in quiz progress: {field}")
                return create_error_response(f"Missing required field: {field}", 400)

        # Log the incoming data (without sensitive info)
        logger.info(f"Storing quiz progress for user: {data.get('user_id')}, type: {data.get('quiz_type')}, score: {data.get('score_percentage')}%")

        # Helper function to parse datetime
        def parse_datetime(datetime_str):
            """Parse datetime string and convert to MySQL format"""
            if not datetime_str:
                return datetime.now()
            
            try:
                # Handle ISO 8601 format
                if isinstance(datetime_str, str):
                    # Remove 'Z' or timezone info for MySQL compatibility
                    datetime_str = datetime_str.replace('Z', '').split('+')[0].split('.')[0]
                    
                    # Parse the datetime using built-in strptime
                    try:
                        parsed_dt = datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S')
                    except ValueError:
                        # Try alternative format
                        parsed_dt = datetime.strptime(datetime_str.split('T')[0], '%Y-%m-%d')
                    
                    return parsed_dt
                
                return datetime_str
                
            except Exception as e:
                logger.warning(f"Failed to parse datetime '{datetime_str}': {e}. Using current time.")
                return datetime.now()

        with get_db_connection() as conn:
            cursor = conn.cursor()

            # Parse completed_at datetime
            completed_at = parse_datetime(data.get('completed_at'))

            # Store main quiz session with enhanced error handling
            quiz_session_query = """
            INSERT INTO quiz_sessions (
                user_id, quiz_type, quiz_id, course_id, topic_ids,
                total_questions, correct_answers, score_percentage,
                time_taken, completed_at, created_at, modelset_id,
                marks_obtained, total_marks_possible, negative_marking,
                negative_marking_percentage, is_ai_generated, ai_quiz_metadata
            ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), %s, %s, %s, %s, %s, %s, %s)
            """
            
            try:
                cursor.execute(quiz_session_query, (
                    data['user_id'],
                    data['quiz_type'],
                    data.get('quiz_id'),
                    data.get('course_id'),
                    json.dumps(data.get('topic_ids', [])),
                    data['total_questions'],
                    data['correct_answers'],
                    data['score_percentage'],
                    data.get('time_taken', 0),
                    completed_at,  # Use parsed datetime
                    data.get('model_set_id'),  # For model set quizzes
                    data.get('total_marks_obtained', 0),  # Actual marks obtained
                    data.get('total_marks_possible', 0),  # Total possible marks
                    data.get('negative_marking', False),  # Negative marking enabled
                    data.get('negative_marking_percentage', 0),  # Negative marking percentage
                    data.get('is_ai_generated', False),  # AI generated quiz
                    json.dumps(data.get('ai_quiz_metadata', {}))  # AI quiz metadata
                ))
                
                session_id = cursor.lastrowid
                logger.info(f"Created quiz session with ID: {session_id}")
                
            except mysql.connector.Error as e:
                logger.error(f"Error inserting quiz session: {e}")
                raise Exception(f"Failed to store quiz session: {str(e)}")
            
            # Store individual question responses with enhanced error handling
            questions_stored = 0
            if 'questions_data' in data and data['questions_data']:
                question_responses_query = """
                INSERT INTO quiz_question_responses (
                    session_id, question_id, selected_option, correct_option, 
                    is_correct, time_taken, topic_id, selected_option_text, 
                    correct_option_text, all_options, created_at
                ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
                """
                
                for idx, question in enumerate(data['questions_data']):
                    try:
                        cursor.execute(question_responses_query, (
                            session_id,
                            question.get('question_id', f'q_{idx}'),
                            question.get('selected_option', 'Not Answered'),
                            question.get('correct_option', ''),
                            question.get('is_correct', False),
                            question.get('time_taken', 0),
                            question.get('topic_id'),
                            question.get('selected_option_text', 'Not Answered'),
                            question.get('correct_option_text', ''),
                            json.dumps(question.get('all_options', []))
                        ))
                        questions_stored += 1
                    except mysql.connector.Error as e:
                        logger.error(f"Error storing question response {idx}: {e}")
                        # Continue with other questions even if one fails
                        
                logger.info(f"Stored {questions_stored} question responses out of {len(data['questions_data'])}")

            # Update user analytics summary with error handling
            try:
                from routes.analytics_routes import update_user_analytics_summary
                update_user_analytics_summary(cursor, data['user_id'], data)
                logger.info(f"Updated analytics summary for user: {data['user_id']}")
            except Exception as e:
                logger.error(f"Error updating user analytics summary: {e}")
                # This is not critical, so we continue

            conn.commit()
            
            response_data = {
                'session_id': session_id,
                'questions_stored': questions_stored,
                'message': f'Quiz progress stored successfully. Session ID: {session_id}'
            }
            
            
            logger.info(f"Successfully stored quiz progress for user {data['user_id']}: {response_data}")
            return format_api_response(
                data=response_data,
                message="Quiz progress stored successfully",
                include_legacy_fields=response_data
            )

    except mysql.connector.Error as db_error:
        logger.error(f"Database error in store_quiz_progress: {str(db_error)}")
        return create_error_response(f"Database error: {str(db_error)}", 500)
    except Exception as e:
        logger.error(f"Unexpected error in store_quiz_progress: {str(e)}")
        return create_error_response(f"Failed to store quiz progress: {str(e)}", 500)

@quiz_bp.route('/api/ai-quiz/generate', methods=['POST'])
@require_api_key
@encrypt_response
def generate_ai_quiz():
    """Generate a quiz using AI based on selected topics with retry mechanism and model selection"""
    conn = None
    cursor = None
    try:
        if not ai_available:
            return create_error_response("AI service is not available", 503)
        
        data = request.get_json() or {}
        user_id = data.get('user_id')
        topic_ids = data.get('topic_ids', [])
        question_count = min(int(data.get('question_count', 10)), 20)  # Max 20 AI questions
        difficulty_level = data.get('difficulty_level', 'medium')
        custom_instructions = data.get('custom_instructions', '')
        negative_marking = data.get('negative_marking', False)
        negative_marking_percentage = data.get('negative_marking_percentage', 25)
        ai_model = data.get('ai_model') or get_default_model(user_id=user_id)  # Use user's preferred model or default
        use_user_questions = data.get('use_user_questions', False)  # Use user's AI questions vs topic questions
        
        if not user_id:
            return create_error_response("User ID is required", 400)
        
        if not topic_ids:
            return create_error_response("At least one topic must be selected", 400)
        
        # Get database connection
        from database import get_cached_connection
        conn = get_cached_connection()
        if not conn:
            return create_error_response("Database connection failed", 500)
        
        # Ensure UTF-8 encoding for proper Nepali/Devanagari text and LaTeX storage
        cursor = conn.cursor(dictionary=True, buffered=True)
        cursor.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
        cursor.execute("SET character_set_client = utf8mb4")
        cursor.execute("SET character_set_results = utf8mb4")
        cursor.execute("SET character_set_connection = utf8mb4")
        cursor.execute("SET collation_connection = utf8mb4_unicode_ci")
        
        # Verify user has access to these topics and get topic details
        placeholders = ','.join(['%s'] * len(topic_ids))
        cursor.execute(f"""
            SELECT t.id, t.title, c.title as course_title
            FROM topics t
            JOIN courses c ON t.course_id = c.id
            JOIN enrollments e ON c.id = e.course_id AND e.user_id = %s AND e.payment_status IN ('free', 'paid')
            WHERE t.id IN ({placeholders})
        """, [user_id] + topic_ids)
        
        accessible_topics = cursor.fetchall()
        
        if not accessible_topics:
            return create_error_response("No accessible topics found", 403)
        
        # Prepare topic information for AI
        topic_info = []
        for topic in accessible_topics:
            topic_info.append(f"- {topic['title']} (from {topic['course_title']})")
        
        topics_text = "\n".join(topic_info)
        
        ai_prompt = f"""
SYSTEM INSTRUCTION: You are a strict JSON-only API. You must output a single JSON array containing exactly {question_count} objects.
DO NOT include markdown formatting (like ```json).
DO NOT include any conversational text (like "Here is the quiz").
DO NOT include trailing commas.

TOPICS:
{topics_text}

JSON STRUCTURE REQUIREMENTS:
1. "question_text": The question string. Use **bold** for keywords.
2. "options": An array of exactly 4 strings.
3. "correct_answer_index": Integer (0-3).
4. "explanation": Detailed explanation. Use LaTeX for math.
5. "difficulty": "{difficulty_level}"
6. "marks": 1

LATEX & ESCAPING RULES:
- For math, use LaTeX.
- CRITICAL: You must DOUBLE-ESCAPE backslashes for JSON. 
  Example: To write LaTeX $\\sin(x)$, you must write "$\\\\sin(x)$" in the JSON string.
- Quote keys and string values with double quotes (").

CONTENT INSTRUCTIONS:
{f'Custom instructions: {custom_instructions}' if custom_instructions else ''}
- Difficulty: {difficulty_level}
- Ensure options are distinct.

EXAMPLE OUTPUT FORMAT (Follow this exactly):
[
  {{
    "question_text": "Calculate the integral of $f(x) = 2x$.",
    "options": ["$x^2 + C$", "$2x^2 + C$", "$x + C$", "$x^2$"],
    "correct_answer_index": 0,
    "explanation": "Using the power rule, $\\\\int 2x dx = 2 \\\\cdot \\\\frac{{x^2}}{{2}} = x^2 + C$.",
    "difficulty": "medium",
    "marks": 1
  }}
]
"""
        
        logger.info(f"Generating AI quiz for user {user_id} with topics: {topic_ids}")
        
        try:
            from utils.async_runner import run_async
            
            # Use a strict system message
            messages = [
                {"role": "system", "content": "You are a JSON API. Output ONLY a raw JSON array. No Markdown. No text. Escape all backslashes."},
                {"role": "user", "content": ai_prompt}
            ]
            
            # Use the new retry function that includes JSON parsing
            ai_questions = run_async(
                generate_and_parse_ai_quiz_with_retry(ai_model, messages, user_id=user_id, max_retries=5)
            )

            
            # Store AI-generated questions in database (questions are already validated)
            stored_questions = []
            quiz_id = str(uuid.uuid4())
            
            for i, ai_question in enumerate(ai_questions):
                try:
                    # Minimal validation - only check if required fields exist, NO content processing
                    if 'question_text' not in ai_question or 'options' not in ai_question or 'correct_answer_index' not in ai_question:
                        logger.warning(f"Question {i} missing required fields, skipping")
                        continue
                    
                    
                    # Log question storage without Nepali text to avoid encoding issues
                    logger.info(f"Storing question {i}: {len(ai_question['question_text'])} chars, {len(ai_question.get('options', []))} options")
                    
                    # Store in questions table with user_id and ai_model (with error handling)
                    try:
                        cursor.execute("""
                            INSERT INTO questions (
                                question_text, options, correct_answer_index, 
                                difficulty, explanation, topic_id, is_ai_generated,
                                ai_metadata, marks, user_id, ai_model, created_at
                            ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW())
                        """, (
                            ai_question['question_text'],  # Store EXACT AI text - no modifications
                            json.dumps(ai_question['options'], ensure_ascii=False, separators=(',', ':')),  # Store with proper UTF-8 encoding
                            int(ai_question['correct_answer_index']),
                            ai_question.get('difficulty', difficulty_level),
                            ai_question['explanation'],  # Store EXACT AI explanation
                            accessible_topics[i % len(accessible_topics)]['id'],  # Distribute among topics
                            True,  # is_ai_generated
                            json.dumps({
                                'quiz_id': quiz_id,
                                'topic_focus': ai_question.get('topic_focus', ''),
                                'generated_at': datetime.now().isoformat(),
                                'model_used': ai_model,
                                'custom_instructions': custom_instructions
                            }),
                            ai_question.get('marks', 1),  # Default to 1 mark per question
                            user_id,  # Store user who generated this question
                            ai_model  # Store the AI model used
                        ))
                    except Exception as db_error:
                        logger.error(f"Failed to store question {i} in database: {db_error}")
                        # Skip this question but continue with others
                        continue
                    
                    question_id = cursor.lastrowid
                    
                    # Format for frontend using the same robust method as analytics routes
                    formatted_options = []
                    try:
                        if isinstance(ai_question['options'], list):
                            correct_index = int(ai_question['correct_answer_index'])
                            
                            for opt_idx, option_text in enumerate(ai_question['options']):
                                option_id = str(opt_idx)  # Use index as option_id
                                is_correct = opt_idx == correct_index
                                
                                formatted_options.append({
                                    'option_id': option_id,
                                    'option_text': str(option_text),  # Ensure it's a string like analytics
                                    'is_correct': is_correct
                                })
                        else:
                            raise ValueError("AI options is not a list")
                    except (ValueError, TypeError, KeyError) as e:
                        logger.warning(f"Error processing AI options for question {i}: {e}")
                        # Fallback options with proper structure
                        for fallback_idx in range(4):
                            formatted_options.append({
                                'option_id': str(fallback_idx),
                                'option_text': f'AI Option {fallback_idx + 1}',
                                'is_correct': fallback_idx == 0
                            })
                    
                    # Log what we're returning to frontend
                    # Log final formatted data for debugging without Nepali text
                    logger.info(f"Returning to frontend - Question {i}: {len(formatted_options)} formatted options")
                    
                    stored_questions.append({
                        'question_id': str(question_id),
                        'question_text': ai_question['question_text'],
                        'options': formatted_options,
                        'difficulty': ai_question.get('difficulty', difficulty_level),
                        'explanation': ai_question['explanation'],
                        'topic_title': accessible_topics[i % len(accessible_topics)]['title'],
                        'topic_id': accessible_topics[i % len(accessible_topics)]['id'],
                        'course_title': accessible_topics[i % len(accessible_topics)]['course_title'],
                        'marks': ai_question.get('marks', 1)
                    })
                    
                except Exception as e:
                    logger.error(f"Error storing AI question {i}: {e}")
                    continue
            
            conn.commit()
            
            if not stored_questions:
                return create_error_response("Failed to generate valid questions", 500)
            
            # Generate quiz metadata
            quiz_data = {
                'quiz_id': quiz_id,
                'title': f"AI-Generated Quiz - {len(accessible_topics)} Topics",
                'description': f"AI-generated quiz with {len(stored_questions)} questions covering selected topics",
                'questions': stored_questions,
                'total_questions': len(stored_questions),
                'time_limit': len(stored_questions) * 90,  # 1.5 minutes per question
                'difficulty_level': difficulty_level,
                'topics_covered': [t['id'] for t in accessible_topics],
                'created_at': datetime.now().isoformat(),
                'is_ai_generated': True,
                'negative_marking': negative_marking,
                'negative_marking_percentage': negative_marking_percentage if negative_marking else 0
            }
            
            logger.info(f"Successfully generated AI quiz with {len(stored_questions)} questions")
            return format_api_response(
                data=quiz_data,
                message="AI quiz generated successfully",
                include_legacy_fields=quiz_data
            )
        
        except Exception as ai_error:
            logger.error(f"AI generation error: {str(ai_error)}")
            
            # Provide more specific error messages
            if "JSON parsing" in str(ai_error):
                error_message = "AI generated invalid format. The model may be having issues with JSON formatting. Please try again or select a different model."
            elif "timeout" in str(ai_error).lower():
                error_message = "AI generation timed out. Please try again with fewer questions or select a faster model."
            elif "All attempts failed" in str(ai_error):
                error_message = "AI generation failed after multiple attempts. Please try again later or contact support if the issue persists."
            else:
                error_message = f"Failed to generate AI quiz: {str(ai_error)}"
            
            return create_error_response(error_message, 500)
            
    except Exception as e:
        logger.error(f"Error in generate_ai_quiz: {str(e)}")
        return create_error_response(f"Failed to generate AI quiz: {str(e)}")
    finally:
        if cursor:
            cursor.close()
        if conn:
            try:
                conn.close()
            except Exception:
                pass


@quiz_bp.route('/health/database', methods=['GET'])
def database_health_check():
    """
    Database health check endpoint for monitoring.
    """
    try:
        from database import check_database_health
        from db_monitor import get_monitoring_status
        
        health_status = check_database_health()
        monitoring_status = get_monitoring_status()
        
        response_data = {
            'database': health_status,
            'monitoring': monitoring_status,
            'timestamp': datetime.now().isoformat()
        }
        
        status_code = 200 if health_status['status'] == 'healthy' else 503
        return format_api_response(response_data, include_legacy_fields=response_data)
        
    except Exception as e:
        error_data = {
            'database': {
                'status': 'error',
                'message': f'Health check failed: {str(e)}'
            },
            'timestamp': datetime.now().isoformat()
        }
        return create_error_response(f'Health check failed: {str(e)}', 500)