<?php
/**
 * Core quiz generation engine.
 *
 * @package MasterQuiz
 * @since   1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class MasterQuiz_Quiz_Generator
 *
 * Orchestrates AI-powered quiz generation from PDF text.
 * Supports 2-call (fast) and 3-phase (quality) modes.
 *
 * Fast mode (2-call):
 *   Call 1: Extract core concepts + generate draft questions.
 *   Call 2: Enrich drafts into final quiz (minimal output, no rationales).
 *
 * Quality mode (3-phase):
 *   Phase 1: Extract core concepts.
 *   Phase 2: Generate draft questions from concepts.
 *   Phase 3: Enrich into full quiz with rationales, model answers, rubrics.
 *
 * @since 1.0.0
 */
class MasterQuiz_Quiz_Generator {

	/**
	 * Provider chain for cascading AI calls.
	 *
	 * @var MasterQuiz_Provider_Chain
	 */
	private $chain;

	/**
	 * RAG client for intelligent chunk selection.
	 *
	 * @var MasterQuiz_Rag_Client
	 */
	private $rag;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {
		$this->chain = new MasterQuiz_Provider_Chain();
		$this->rag   = new MasterQuiz_Rag_Client();
	}

	/**
	 * Generate a quiz from PDF text.
	 *
	 * @since 1.0.0
	 *
	 * @param string $pdf_text Extracted text from the PDF.
	 * @param array  $args {
	 *     Generation arguments.
	 *
	 *     @type int    $num_questions  Number of questions (3-5).
	 *     @type array  $question_types Question type slugs (mcq, true_false, essay).
	 *     @type string $quiz_mode      'fast' or 'quality'.
	 *     @type string $quiz_title     Title for the quiz.
	 * }
	 * @return array|WP_Error Quiz data array or error.
	 */
	public function generate_quiz( $pdf_text, $args = array() ) {
		$defaults = array(
			'num_questions'  => absint( get_option( 'masterquiz_default_num_questions', 5 ) ),
			'question_types' => get_option( 'masterquiz_default_question_types', array( 'mcq', 'true_false', 'essay' ) ),
			'quiz_mode'      => sanitize_text_field( get_option( 'masterquiz_default_quiz_mode', 'fast' ) ),
			'quiz_title'     => __( 'Generated Quiz', 'masterquiz-ai' ),
		);

		$args           = wp_parse_args( $args, $defaults );
		// Hard cap at 5 even if a larger value is posted manually.
		$num_questions  = max( 3, min( 5, absint( $args['num_questions'] ) ) );
		$question_types = $this->normalize_question_types( $args['question_types'] );
		$quiz_mode      = in_array( $args['quiz_mode'], array( 'fast', 'quality' ), true ) ? $args['quiz_mode'] : 'fast';
		$title          = sanitize_text_field( $args['quiz_title'] );
		if ( empty( $title ) ) {
			$title = __( 'Generated Quiz', 'masterquiz-ai' );
		}
		$is_fast        = ( 'fast' === $quiz_mode );

		if ( empty( $pdf_text ) ) {
			return new WP_Error( 'empty_text', esc_html__( 'PDF text is empty.', 'masterquiz-ai' ) );
		}

		MasterQuiz_Logger::info( 'Starting quiz generation.', array(
			'mode'       => $quiz_mode,
			'questions'  => $num_questions,
			'types'      => $question_types,
			'text_len'   => strlen( $pdf_text ),
		) );

		// Select relevant context from PDF text.
		$context    = $this->select_context( $pdf_text, $num_questions, $is_fast );
		$rag_method = $context['method'];
		$context    = $context['text'];

		// Route to the appropriate generation method.
		if ( $is_fast || 'fast' === $quiz_mode ) {
			$result = $this->generate_1call( $context, $title, $num_questions, $question_types );
		} else {
			$result = $this->generate_3phase( $context, $title, $num_questions, $question_types );
		}

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		// Add metadata.
		$result['quiz_title']    = $title;
		$result['total_points']  = 100;
		$result['question_count'] = count( $result['questions'] );
		$result['rag_method']    = $rag_method;
		$result['model']         = $this->chain->get_used_model();

		MasterQuiz_Logger::info( 'Quiz generation completed.', array(
			'model'     => $result['model'],
			'questions' => $result['question_count'],
		) );

		return $result;
	}

	/**
	 * Select relevant context from PDF text via RAG or local chunking.
	 *
	 * @since 1.0.0
	 *
	 * @param string $pdf_text      Full PDF text.
	 * @param int    $num_questions Number of questions.
	 * @param bool   $is_fast       Whether fast mode.
	 * @return array Array with 'text' and 'method' keys.
	 */
	private function select_context( $pdf_text, $num_questions, $is_fast ) {
		// Fast mode: always local.
		if ( $is_fast ) {
			$max_chunks = min( 2, max( 1, (int) ceil( $num_questions / 3 ) ) );
			$max_chars  = $num_questions <= 5 ? 2800 : 4500;
			$chunks     = $this->select_relevant_chunks( $pdf_text, $max_chunks, $max_chars );

			return array(
				'text'   => implode( "\n\n", $chunks ),
				'method' => 'local-fast',
			);
		}

		// Quality mode: try RAG, fallback to local.
		if ( $this->rag->is_enabled() ) {
			$rag_result = $this->rag->select_chunks( $pdf_text, min( 5, $num_questions ), 12000 );
			if ( null !== $rag_result && ! empty( $rag_result['context'] ) ) {
				return array(
					'text'   => $rag_result['context'],
					'method' => isset( $rag_result['method'] ) ? $rag_result['method'] : 'rag',
				);
			}
		}

		$max_chars = $num_questions <= 5 ? 8000 : 12000;
		$chunks    = $this->select_relevant_chunks( $pdf_text, min( 5, $num_questions ), $max_chars );

		return array(
			'text'   => implode( "\n\n", $chunks ),
			'method' => 'local',
		);
	}


	/**
	 * Single-call generation (fast mode).
	 *
	 * One AI request produces the complete quiz — roughly half the latency of 2-call.
	 *
	 * @since 1.0.1
	 *
	 * @param string $context        Selected text context.
	 * @param string $title          Quiz title.
	 * @param int    $num_questions  Number of questions.
	 * @param array  $question_types Question type list.
	 * @return array|WP_Error Quiz data or error.
	 */
	private function generate_1call( $context, $title, $num_questions, $question_types ) {
		$gen_start        = microtime( true );
		$type_instruction = $this->build_type_instruction( $question_types );
		$schema           = $this->build_fast_schema( $num_questions );

		$prompt = 'Quiz "' . $title . '": EXACTLY ' . $num_questions . ' questions.' . $type_instruction
			. ' MCQ=4 options (A-D), one correct. T/F=True+False only. Essay=empty options. Points=100. Short hints.'
			. "\n\nCONTENT:\n" . $context;

		$call_start = microtime( true );
		$raw        = $this->chain->call( $prompt, $schema, 'fast', 'single_call' );
		MasterQuiz_Logger::info( 'Single-call timing.', array( 'seconds' => round( microtime( true ) - $call_start, 2 ) ) );

		if ( is_wp_error( $raw ) ) {
			return $raw;
		}

		$final = $this->extract_json( $raw );
		if ( empty( $final ) || ! isset( $final['questions'] ) || ! is_array( $final['questions'] ) ) {
			return new WP_Error( 'parse_failed', esc_html__( 'The AI returned an unexpected response. Please try again.', 'masterquiz-ai' ) );
		}

		$core_concepts = array();
		if ( ! empty( $final['core_concepts'] ) && is_array( $final['core_concepts'] ) ) {
			$core_concepts = $final['core_concepts'];
		}

		$final['questions'] = $this->post_process_questions( $final['questions'], true );
		$final['questions'] = $this->normalize_points( $final['questions'] );

		MasterQuiz_Logger::info( 'Total 1-call generation timing.', array( 'seconds' => round( microtime( true ) - $gen_start, 2 ) ) );

		return array(
			'core_concepts' => $core_concepts,
			'questions'     => $final['questions'],
		);
	}

	/**
	 * Minimal JSON schema for single-call fast generation.
	 *
	 * @since 1.0.1
	 *
	 * @param int $num_questions Number of questions.
	 * @return array JSON schema array.
	 */
	private function build_fast_schema( $num_questions ) {
		return array(
			'type'       => 'object',
			'properties' => array(
				'quiz_title'    => array( 'type' => 'string' ),
				'core_concepts' => array(
					'type'     => 'array',
					'items'    => array( 'type' => 'string' ),
					'maxItems' => 5,
				),
				'questions'     => array(
					'type'     => 'array',
					'minItems' => $num_questions,
					'maxItems' => $num_questions,
					'items'    => array(
						'type'       => 'object',
						'properties' => array(
							'id'            => array( 'type' => 'string' ),
							'type'          => array( 'type' => 'string' ),
							'points'        => array( 'type' => 'integer' ),
							'question_text' => array( 'type' => 'string' ),
							'hint'          => array( 'type' => 'string' ),
							'options'       => array(
								'type'  => 'array',
								'items' => array(
									'type'       => 'object',
									'properties' => array(
										'id'      => array( 'type' => 'string' ),
										'text'    => array( 'type' => 'string' ),
										'correct' => array( 'type' => 'boolean' ),
									),
									'required'   => array( 'id', 'text', 'correct' ),
								),
							),
						),
						'required'   => array( 'id', 'type', 'points', 'question_text', 'hint', 'options' ),
					),
				),
			),
			'required'   => array( 'quiz_title', 'questions' ),
		);
	}

	/**
	 * 2-call generation (fast mode).
	 *
	 * Call 1: Concepts + drafts.
	 * Call 2: Final enrichment.
	 *
	 * @since 1.0.0
	 *
	 * @param string $context        Selected text context.
	 * @param string $title          Quiz title.
	 * @param int    $num_questions  Number of questions.
	 * @param array  $question_types Question type list.
	 * @param bool   $is_fast        Whether fast mode.
	 * @return array|WP_Error Quiz data or error.
	 */
	private function generate_2call( $context, $title, $num_questions, $question_types, $is_fast ) {

		$gen_start = microtime( true );

		// --- Call 1: Concepts + Draft Questions ---
		$type_instruction = $this->build_type_instruction( $question_types );
		$schema_1 = array(
			'type'       => 'object',
			'properties' => array(
				'core_concepts'   => array(
					'type'     => 'array',
					'items'    => array( 'type' => 'string' ),
					'minItems' => 6,
					'maxItems' => 10,
				),
				'draft_questions' => array(
					'type'     => 'array',
					'minItems' => $num_questions,
					'maxItems' => $num_questions,
					'items'    => array(
						'type'       => 'object',
						'properties' => array(
							'id'              => array( 'type' => 'string' ),
							'type'            => array( 'type' => 'string' ),
							'cognitive_level' => array( 'type' => 'string' ),
							'question_text'   => array( 'type' => 'string' ),
							'concept_tags'    => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
							'hint'            => array( 'type' => 'string' ),
						),
						'required'   => array( 'id', 'type', 'cognitive_level', 'question_text', 'concept_tags', 'hint' ),
					),
				),
			),
			'required'   => array( 'core_concepts', 'draft_questions' ),
		);

		$prompt_1 = 'Extract 6-10 foundational core concepts STRICTLY from the provided content (terms/phrases explicitly present). Then, generate EXACTLY '
			. $num_questions . ' high-cognitive draft questions' . $type_instruction
			. ' IMPORTANT: Distribute questions evenly across the specified types. Include hints and concept_tags drawn only from those concepts. Focus on synthesis, analysis, comparison. Return JSON with core_concepts and draft_questions arrays.'
			. "\n\nCONTENT:\n" . $context;

		$call1_start = microtime( true );
		$raw_1 = $this->chain->call( $prompt_1, $schema_1, $is_fast ? 'fast' : 'quality', 'concepts_and_drafts' );
		MasterQuiz_Logger::info( 'Call 1 timing.', array( 'seconds' => round( microtime( true ) - $call1_start, 2 ) ) );

		if ( is_wp_error( $raw_1 ) ) {
			return $raw_1;
		}

		$data_1 = $this->extract_json( $raw_1 );
		if ( empty( $data_1 ) ) {
			return new WP_Error( 'parse_failed', esc_html__( 'The AI returned an unexpected response. Please try again.', 'masterquiz-ai' ) );
		}

		$core_concepts   = isset( $data_1['core_concepts'] ) && is_array( $data_1['core_concepts'] ) ? $data_1['core_concepts'] : array();
		$draft_questions = isset( $data_1['draft_questions'] ) && is_array( $data_1['draft_questions'] ) ? $data_1['draft_questions'] : array();

		if ( count( $core_concepts ) < 3 ) {
			return new WP_Error( 'insufficient_concepts', esc_html__( 'Not enough content found in the PDF to generate questions. Please try a longer document.', 'masterquiz-ai' ) );
		}

		if ( count( $draft_questions ) < 1 ) {
			return new WP_Error( 'no_drafts', esc_html__( 'Could not generate questions from this PDF. Please try again.', 'masterquiz-ai' ) );
		}

		// --- Call 2: Final Enrichment ---
		$concepts_joined = implode( ', ', $core_concepts );
		$draft_json      = wp_json_encode( $draft_questions );

		$schema_2 = $this->build_final_schema( $num_questions, $is_fast );

		if ( $is_fast ) {
			$prompt_2 = "Transform drafts into FINAL QUIZ '" . $title . "'. Add points only. "
				. "For multiple_choice: exactly 4 options (A, B, C, D) with id, text, correct only - NO rationales. "
				. "For true_false: EXACTLY 2 options - 'True' and 'False' only. "
				. "Do NOT include model_answer, rubric_criteria, or any option rationales. Output minimal JSON only. Concepts: "
				. $concepts_joined . '. Drafts: ' . $draft_json . '. Return JSON only.';
		} else {
			$prompt_2 = "Transform drafts into FINAL QUIZ '" . $title . "'. Add points. "
				. "For multiple_choice questions: generate exactly 4 options (A, B, C, D) with rationales. "
				. "For true_false questions: generate EXACTLY 2 options - 'True' and 'False' only. Include rationales for both options. "
				. "For essay/comparative questions: include model_answer and rubric_criteria. "
				. "Preserve question types. Concepts: " . $concepts_joined . '. Drafts: ' . $draft_json . '. Return JSON only.';
		}

		$phase2_cap     = $is_fast ? 3500 : 6000;
		$final_context  = strlen( $context ) > $phase2_cap ? substr( $context, 0, $phase2_cap ) . '...' : $context;
		$prompt_2      .= "\n\nCONTENT:\n" . $final_context;

		$call2_start = microtime( true );
		$raw_2 = $this->chain->call( $prompt_2, $schema_2, $is_fast ? 'fast' : 'quality', 'final_quiz' );
		MasterQuiz_Logger::info( 'Call 2 timing.', array( 'seconds' => round( microtime( true ) - $call2_start, 2 ) ) );

		if ( is_wp_error( $raw_2 ) ) {
			return $raw_2;
		}

		$final = $this->extract_json( $raw_2 );
		if ( empty( $final ) || ! isset( $final['questions'] ) || ! is_array( $final['questions'] ) ) {
			return new WP_Error( 'parse_failed', esc_html__( 'The AI returned an unexpected response. Please try again.', 'masterquiz-ai' ) );
		}

		// Post-process.
		$final['questions'] = $this->post_process_questions( $final['questions'], $is_fast );
		$final['questions'] = $this->normalize_points( $final['questions'] );

		MasterQuiz_Logger::info( 'Total 2-call generation timing.', array( 'seconds' => round( microtime( true ) - $gen_start, 2 ) ) );

		return array(
			'core_concepts' => $core_concepts,
			'questions'     => $final['questions'],
		);
	}

	/**
	 * 3-phase generation (quality mode).
	 *
	 * Phase 1: Extract core concepts.
	 * Phase 2: Generate draft questions.
	 * Phase 3: Enrich into final quiz.
	 *
	 * @since 1.0.0
	 *
	 * @param string $context        Selected text context.
	 * @param string $title          Quiz title.
	 * @param int    $num_questions  Number of questions.
	 * @param array  $question_types Question type list.
	 * @return array|WP_Error Quiz data or error.
	 */
	private function generate_3phase( $context, $title, $num_questions, $question_types ) {

		// --- Phase 1: Core Concepts ---
		$schema_1 = array(
			'type'       => 'object',
			'properties' => array(
				'core_concepts' => array(
					'type'     => 'array',
					'items'    => array( 'type' => 'string' ),
					'minItems' => 6,
					'maxItems' => 10,
				),
			),
			'required'   => array( 'core_concepts' ),
		);

		$prompt_1 = "Extract 6-10 foundational core concepts STRICTLY from the provided content. Only terms/phrases explicitly present. Return JSON with core_concepts array.\n\nCONTENT:\n" . $context;

		$raw_1 = $this->chain->call( $prompt_1, $schema_1, 'quality', 'concept_extraction' );
		if ( is_wp_error( $raw_1 ) ) {
			return $raw_1;
		}

		$data_1         = $this->extract_json( $raw_1 );
		$core_concepts  = isset( $data_1['core_concepts'] ) && is_array( $data_1['core_concepts'] ) ? $data_1['core_concepts'] : array();

		if ( count( $core_concepts ) < 3 ) {
			return new WP_Error( 'insufficient_concepts', esc_html__( 'Not enough content found in the PDF to generate questions. Please try a longer document.', 'masterquiz-ai' ) );
		}

		// --- Phase 2: Draft Questions ---
		$type_instruction = $this->build_type_instruction( $question_types );
		$concepts_joined  = implode( ', ', $core_concepts );

		$schema_2 = array(
			'type'       => 'object',
			'properties' => array(
				'draft_questions' => array(
					'type'     => 'array',
					'minItems' => $num_questions,
					'maxItems' => $num_questions,
					'items'    => array(
						'type'       => 'object',
						'properties' => array(
							'id'              => array( 'type' => 'string' ),
							'type'            => array( 'type' => 'string' ),
							'cognitive_level' => array( 'type' => 'string' ),
							'question_text'   => array( 'type' => 'string' ),
							'concept_tags'    => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
							'hint'            => array( 'type' => 'string' ),
						),
						'required'   => array( 'id', 'type', 'cognitive_level', 'question_text', 'concept_tags', 'hint' ),
					),
				),
			),
			'required'   => array( 'draft_questions' ),
		);

		$prompt_2 = 'Generate EXACTLY ' . $num_questions . ' high-cognitive draft questions'
			. $type_instruction . ' with hints and concept_tags drawn only from: '
			. $concepts_joined . '. Focus on synthesis, analysis, comparison. JSON only.'
			. "\n\nCONTENT:\n" . $context;

		$raw_2 = $this->chain->call( $prompt_2, $schema_2, 'quality', 'draft_generation' );
		if ( is_wp_error( $raw_2 ) ) {
			return $raw_2;
		}

		$data_2          = $this->extract_json( $raw_2 );
		$draft_questions = isset( $data_2['draft_questions'] ) && is_array( $data_2['draft_questions'] ) ? $data_2['draft_questions'] : array();

		if ( count( $draft_questions ) < 1 ) {
			return new WP_Error( 'no_drafts', esc_html__( 'Could not generate questions from this PDF. Please try again.', 'masterquiz-ai' ) );
		}

		// --- Phase 3: Final Quiz ---
		$draft_json = wp_json_encode( $draft_questions );
		$schema_3   = $this->build_final_schema( $num_questions, false );

		$prompt_3 = "Transform drafts into FINAL QUIZ '" . $title . "'. Add points. "
			. "For multiple_choice: exactly 4 options with rationales. "
			. "For true_false: EXACTLY 2 options - 'True' and 'False' only, with rationales. "
			. "For essay/comparative: include model_answer and rubric_criteria. "
			. "Preserve question types. Concepts: " . $concepts_joined . '. Drafts: ' . $draft_json . '. Return JSON only.';

		$final_context = strlen( $context ) > 6000 ? substr( $context, 0, 6000 ) . '...' : $context;
		$prompt_3     .= "\n\nCONTENT:\n" . $final_context;

		$raw_3 = $this->chain->call( $prompt_3, $schema_3, 'quality', 'final_quiz' );
		if ( is_wp_error( $raw_3 ) ) {
			return $raw_3;
		}

		$final = $this->extract_json( $raw_3 );
		if ( empty( $final ) || ! isset( $final['questions'] ) || ! is_array( $final['questions'] ) ) {
			return new WP_Error( 'parse_failed', esc_html__( 'The AI returned an unexpected response. Please try again.', 'masterquiz-ai' ) );
		}

		$final['questions'] = $this->post_process_questions( $final['questions'], false );
		$final['questions'] = $this->normalize_points( $final['questions'] );

		return array(
			'core_concepts' => $core_concepts,
			'questions'     => $final['questions'],
		);
	}

	/**
	 * Build JSON schema for the final quiz enrichment call.
	 *
	 * @since 1.0.0
	 *
	 * @param int  $num_questions Number of questions.
	 * @param bool $is_fast       Whether to use minimal schema.
	 * @return array JSON schema array.
	 */
	private function build_final_schema( $num_questions, $is_fast ) {
		$option_props = array(
			'id'      => array( 'type' => 'string' ),
			'text'    => array( 'type' => 'string' ),
			'correct' => array( 'type' => 'boolean' ),
		);
		$option_required = array( 'id', 'text', 'correct' );

		if ( ! $is_fast ) {
			$option_props['rationale'] = array( 'type' => 'string' );
			$option_required[]         = 'rationale';
		}

		$question_props = array(
			'id'                 => array( 'type' => 'string' ),
			'type'               => array( 'type' => 'string' ),
			'cognitive_level'    => array( 'type' => 'string' ),
			'points'             => array( 'type' => 'integer' ),
			'question_text'      => array( 'type' => 'string' ),
			'hint'               => array( 'type' => 'string' ),
			'options'            => array(
				'type'     => 'array',
				'items'    => array(
					'type'       => 'object',
					'properties' => $option_props,
					'required'   => $option_required,
				),
				'minItems' => 0,
			),
			'concept_tags'       => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
			'correct_answer_ids' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
		);

		if ( ! $is_fast ) {
			$question_props['model_answer']    = array( 'type' => 'string' );
			$question_props['rubric_criteria'] = array( 'type' => 'array', 'items' => array( 'type' => 'string' ) );
		}

		return array(
			'type'       => 'object',
			'properties' => array(
				'quiz_title'     => array( 'type' => 'string' ),
				'core_concepts'  => array( 'type' => 'array', 'items' => array( 'type' => 'string' ) ),
				'total_points'   => array( 'type' => 'integer' ),
				'mastery_bands'  => array( 'type' => 'object' ),
				'questions'      => array(
					'type'     => 'array',
					'minItems' => $num_questions,
					'maxItems' => $num_questions,
					'items'    => array(
						'type'       => 'object',
						'properties' => $question_props,
						'required'   => array( 'id', 'type', 'cognitive_level', 'points', 'question_text', 'hint' ),
					),
				),
			),
			'required'   => array( 'quiz_title', 'core_concepts', 'questions' ),
		);
	}

	/**
	 * Post-process questions: fix True/False options, populate correct_answer_ids,
	 * strip extras in fast mode.
	 *
	 * @since 1.0.0
	 *
	 * @param array $questions Raw questions array.
	 * @param bool  $is_fast   Whether fast mode.
	 * @return array Processed questions.
	 */
	private function post_process_questions( $questions, $is_fast ) {
		foreach ( $questions as &$q ) {
			$q_type = isset( $q['type'] ) ? strtolower( $q['type'] ) : '';

			// Fix True/False questions to only have True and False options.
			if ( 'true_false' === $q_type && isset( $q['options'] ) && is_array( $q['options'] ) ) {
				$q['options'] = $this->fix_true_false_options( $q['options'] );
			}

			// Populate correct_answer_ids.
			if ( ! isset( $q['correct_answer_ids'] ) && isset( $q['options'] ) && is_array( $q['options'] ) ) {
				$ids = array();
				foreach ( $q['options'] as $opt ) {
					if ( ! empty( $opt['correct'] ) && isset( $opt['id'] ) ) {
						$ids[] = $opt['id'];
					}
				}
				$q['correct_answer_ids'] = $ids;
			}

			// Fast mode: strip model_answer, rubric_criteria, rationales.
			if ( $is_fast ) {
				unset( $q['model_answer'], $q['rubric_criteria'] );
				if ( isset( $q['options'] ) && is_array( $q['options'] ) ) {
					foreach ( $q['options'] as &$opt ) {
						unset( $opt['rationale'] );
					}
					unset( $opt );
				}
			}
		}
		unset( $q );

		return $questions;
	}

	/**
	 * Fix True/False question options to exactly True and False.
	 *
	 * @since 1.0.0
	 *
	 * @param array $options Raw options array.
	 * @return array Cleaned True/False options.
	 */
	private function fix_true_false_options( $options ) {
		$tf_options = array();
		$has_true   = false;
		$has_false  = false;

		foreach ( $options as $opt ) {
			$text = '';
			if ( is_string( $opt ) ) {
				$text = strtolower( trim( $opt ) );
			} elseif ( is_array( $opt ) && isset( $opt['text'] ) ) {
				$text = strtolower( trim( $opt['text'] ) );
			}

			if ( in_array( $text, array( 'true', 't' ), true ) && ! $has_true ) {
				$entry       = is_array( $opt ) ? $opt : array( 'id' => 'true', 'text' => 'True', 'correct' => false );
				$tf_options[] = $entry;
				$has_true     = true;
			} elseif ( in_array( $text, array( 'false', 'f' ), true ) && ! $has_false ) {
				$entry        = is_array( $opt ) ? $opt : array( 'id' => 'false', 'text' => 'False', 'correct' => false );
				$tf_options[] = $entry;
				$has_false    = true;
			}
		}

		if ( ! $has_true ) {
			$tf_options[] = array( 'id' => 'true', 'text' => 'True', 'correct' => false );
		}
		if ( ! $has_false ) {
			$tf_options[] = array( 'id' => 'false', 'text' => 'False', 'correct' => false );
		}

		// Ensure exactly one correct answer.
		$correct_count = 0;
		foreach ( $tf_options as $o ) {
			if ( ! empty( $o['correct'] ) ) {
				$correct_count++;
			}
		}

		if ( 0 === $correct_count ) {
			$tf_options[0]['correct'] = true;
		} elseif ( $correct_count > 1 ) {
			$found = false;
			foreach ( $tf_options as &$o ) {
				if ( ! empty( $o['correct'] ) && ! $found ) {
					$found = true;
				} elseif ( ! empty( $o['correct'] ) ) {
					$o['correct'] = false;
				}
			}
			unset( $o );
		}

		return $tf_options;
	}

	/**
	 * Assign equal points to every question so they sum to exactly 100.
	 *
	 * Ignores any AI-suggested per-question / per-type weights (MCQ vs essay
	 * vs true/false). Remainder from integer division goes to the first questions.
	 *
	 * @since 1.0.0
	 *
	 * @param array $questions Questions array.
	 * @return array Questions with equalized points.
	 */
	private function normalize_points( $questions ) {
		if ( empty( $questions ) || ! is_array( $questions ) ) {
			return $questions;
		}

		$count     = count( $questions );
		$base      = (int) floor( 100 / $count );
		$remainder = 100 - ( $base * $count );

		foreach ( $questions as $i => &$q ) {
			$q['points'] = $base + ( $i < $remainder ? 1 : 0 );
		}
		unset( $q );

		return $questions;
	}

	/**
	 * Build type instruction string for prompts.
	 *
	 * @since 1.0.0
	 *
	 * @param array $question_types Array of type slugs.
	 * @return string Instruction text.
	 */
	private function build_type_instruction( $question_types ) {
		if ( empty( $question_types ) ) {
			return ' Generate a mix of question types: multiple_choice, analytical_essay, and comparative_analysis.';
		}

		$valid = array();
		$map   = array(
			'mcq'        => 'multiple_choice',
			'true_false' => 'true_false',
			'essay'      => 'analytical_essay',
		);

		foreach ( $question_types as $type ) {
			if ( isset( $map[ $type ] ) ) {
				$valid[] = $map[ $type ];
			} elseif ( in_array( $type, array( 'multiple_choice', 'true_false', 'analytical_essay', 'comparative_analysis' ), true ) ) {
				$valid[] = $type;
			}
		}

		if ( empty( $valid ) ) {
			return ' Generate a mix of question types: multiple_choice, analytical_essay, and comparative_analysis.';
		}

		return ' Generate ONLY these question types: ' . implode( ', ', array_unique( $valid ) ) . '. Distribute questions across these types.';
	}

	/**
	 * Normalize question type slugs from frontend to internal format.
	 *
	 * @since 1.0.0
	 *
	 * @param array $types Raw type slugs.
	 * @return array Sanitized type slugs.
	 */
	private function normalize_question_types( $types ) {
		if ( ! is_array( $types ) ) {
			return array( 'mcq', 'true_false', 'essay' );
		}

		$allowed = array( 'mcq', 'true_false', 'essay' );
		$result  = array();

		foreach ( $types as $type ) {
			$type = sanitize_text_field( $type );
			if ( in_array( $type, $allowed, true ) ) {
				$result[] = $type;
			}
		}

		return ! empty( $result ) ? $result : array( 'mcq', 'true_false', 'essay' );
	}

	/**
	 * Select most relevant chunks using term frequency scoring.
	 *
	 * @since 1.0.0
	 *
	 * @param string $text               Full text.
	 * @param int    $max_chunks         Maximum chunks to select.
	 * @param int    $max_chars_per_chunk Maximum characters per chunk.
	 * @return array Selected text chunks.
	 */
	private function select_relevant_chunks( $text, $max_chunks = 5, $max_chars_per_chunk = 12000 ) {
		$sentences = preg_split( '/(?<=[.!?])\s+/', $text );
		$chunks    = array();
		$current   = '';

		foreach ( $sentences as $s ) {
			if ( strlen( $current ) + strlen( $s ) > $max_chars_per_chunk && '' !== $current ) {
				$chunks[] = trim( $current );
				$current  = $s;
			} else {
				$current .= ' ' . $s;
			}
		}
		if ( '' !== $current ) {
			$chunks[] = trim( $current );
		}

		if ( count( $chunks ) <= $max_chunks ) {
			return $chunks;
		}

		// Score by token frequency.
		$global_freq = array();
		$scores      = array();

		foreach ( $chunks as $idx => $chunk ) {
			$tokens = preg_split( '/[^a-zA-Z0-9]+/', strtolower( $chunk ) );
			$local  = array();
			foreach ( $tokens as $t ) {
				if ( strlen( $t ) < 5 ) {
					continue;
				}
				$global_freq[ $t ] = isset( $global_freq[ $t ] ) ? $global_freq[ $t ] + 1 : 1;
				$local[ $t ]       = isset( $local[ $t ] ) ? $local[ $t ] + 1 : 1;
			}
			$scores[ $idx ] = array_sum( $local );
		}

		// Boost rarer but descriptive tokens.
		foreach ( $scores as $idx => $base ) {
			$tokens = preg_split( '/[^a-zA-Z0-9]+/', strtolower( $chunks[ $idx ] ) );
			$boost  = 0;
			foreach ( $tokens as $t ) {
				if ( strlen( $t ) < 7 ) {
					continue;
				}
				$freq   = isset( $global_freq[ $t ] ) ? $global_freq[ $t ] : 1;
				$boost += 1 / min( $freq, 10 );
			}
			$scores[ $idx ] = $base + $boost;
		}

		arsort( $scores );
		$selected = array();
		foreach ( $scores as $idx => $score ) {
			$selected[] = $chunks[ $idx ];
			if ( count( $selected ) >= $max_chunks ) {
				break;
			}
		}

		return $selected;
	}

	/**
	 * Extract JSON from a raw AI response (strip markdown fences, find JSON object).
	 *
	 * @since 1.0.0
	 *
	 * @param string $raw Raw response string.
	 * @return array|null Decoded JSON array or null.
	 */
	private function extract_json( $raw ) {
		$clean = preg_replace( '/```json\s*/', '', $raw );
		$clean = preg_replace( '/```\s*/', '', $clean );
		$clean = trim( $clean );

		if ( preg_match( '/\{.*\}/s', $clean, $m ) ) {
			$clean = $m[0];
		}

		$data = json_decode( $clean, true );

		return is_array( $data ) ? $data : null;
	}
}
