<?php
/**
 * Quiz export handler.
 *
 * @package MasterQuiz
 * @since   1.0.0
 */

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

/**
 * Class MasterQuiz_Exporter
 *
 * Exports quiz data to 9 different formats: JSON, XML, CSV, TXT, GIFT,
 * QTI, Aiken, LearnDash CSV, and LearnDash XML.
 *
 * @since 1.0.0
 */
class MasterQuiz_Exporter {

	/**
	 * Valid export formats.
	 *
	 * @var array
	 */
	const FORMATS = array(
		'json'          => 'JSON',
		'xml'           => 'XML',
		'csv'           => 'CSV',
		'txt'           => 'Plain Text',
		'gift'          => 'Moodle GIFT',
		'qti'           => 'QTI 1.2',
		'aiken'         => 'Aiken',
		'learndash_csv' => 'LearnDash CSV',
		'learndash_xml' => 'LearnDash XML',
	);

	/**
	 * Export quiz data in the specified format.
	 *
	 * Sends appropriate headers and outputs the file content. Dies after output.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $quiz_data The quiz data array.
	 * @param string $format    Export format key from self::FORMATS.
	 */
	public function export( $quiz_data, $format ) {
		if ( ! array_key_exists( $format, self::FORMATS ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid export format.', 'masterquiz-ai' ) ) );
			return;
		}

		$questions = $this->get_questions( $quiz_data );
		if ( empty( $questions ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No questions to export.', 'masterquiz-ai' ) ) );
			return;
		}

		$method  = 'to_' . $format;
		$content = $this->$method( $quiz_data, $questions );
		$ext     = $this->get_extension( $format );
		$mime    = $this->get_mime_type( $format );
		$title   = sanitize_file_name( isset( $quiz_data['quiz_title'] ) ? $quiz_data['quiz_title'] : 'quiz' );

		// Sanitize filename for Content-Disposition header to prevent header injection.
		$safe_title = str_replace( array( '"', "\r", "\n" ), '', $title );

		nocache_headers();
		header( 'Content-Type: ' . $mime ); // phpcs:ignore WordPress.Security.SafeRedirect -- file download header.
		header( 'Content-Disposition: attachment; filename="' . $safe_title . '.' . $ext . '"' ); // phpcs:ignore WordPress.Security.SafeRedirect
		header( 'Content-Length: ' . strlen( $content ) ); // phpcs:ignore WordPress.Security.SafeRedirect
		echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- binary file download, content generated internally.
		exit;
	}

	/**
	 * Get available export formats.
	 *
	 * @since 1.0.0
	 *
	 * @return array Format key => label pairs.
	 */
	public static function get_formats() {
		return self::FORMATS;
	}

	/* =============================================
	   Helpers
	   ============================================= */

	/**
	 * Extract questions array from quiz data.
	 *
	 * @param array $quiz_data Quiz data.
	 * @return array Questions.
	 */
	private function get_questions( $quiz_data ) {
		if ( isset( $quiz_data['questions'] ) && is_array( $quiz_data['questions'] ) ) {
			return $quiz_data['questions'];
		}
		if ( is_array( $quiz_data ) && isset( $quiz_data[0] ) ) {
			return $quiz_data;
		}
		return array();
	}

	/**
	 * Get question text from a question array.
	 *
	 * @param array $q Single question.
	 * @return string
	 */
	private function q_text( $q ) {
		return isset( $q['question_text'] ) ? $q['question_text'] : ( isset( $q['question'] ) ? $q['question'] : '' );
	}

	/**
	 * Get MIME type for a format.
	 *
	 * @param string $format Format key.
	 * @return string MIME type.
	 */
	private function get_mime_type( $format ) {
		$types = array(
			'json'          => 'application/json',
			'xml'           => 'application/xml',
			'csv'           => 'text/csv',
			'txt'           => 'text/plain',
			'gift'          => 'text/plain',
			'qti'           => 'application/xml',
			'aiken'         => 'text/plain',
			'learndash_csv' => 'text/csv',
			'learndash_xml' => 'application/xml',
		);
		return isset( $types[ $format ] ) ? $types[ $format ] : 'application/octet-stream';
	}

	/**
	 * Get file extension for a format.
	 *
	 * @param string $format Format key.
	 * @return string Extension.
	 */
	private function get_extension( $format ) {
		$exts = array(
			'json'          => 'json',
			'xml'           => 'xml',
			'csv'           => 'csv',
			'txt'           => 'txt',
			'gift'          => 'txt',
			'qti'           => 'xml',
			'aiken'         => 'txt',
			'learndash_csv' => 'csv',
			'learndash_xml' => 'xml',
		);
		return isset( $exts[ $format ] ) ? $exts[ $format ] : 'txt';
	}

	/* =============================================
	   Format: JSON
	   ============================================= */

	/**
	 * Export to JSON format.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string JSON string.
	 */
	private function to_json( $quiz_data, $questions ) {
		return wp_json_encode( $quiz_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE );
	}

	/* =============================================
	   Format: XML
	   ============================================= */

	/**
	 * Export to generic XML format.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string XML string.
	 */
	private function to_xml( $quiz_data, $questions ) {
		$title = isset( $quiz_data['quiz_title'] ) ? $quiz_data['quiz_title'] : 'Quiz';

		$xml  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		$xml .= '<quiz>' . "\n";
		$xml .= '  <title>' . $this->xml_escape( $title ) . '</title>' . "\n";
		$xml .= '  <questions>' . "\n";

		foreach ( $questions as $i => $q ) {
			$xml .= '    <question index="' . ( $i + 1 ) . '" type="' . esc_attr( $q['type'] ?? 'multiple_choice' ) . '">' . "\n";
			$xml .= '      <text>' . $this->xml_escape( $this->q_text( $q ) ) . '</text>' . "\n";

			if ( ! empty( $q['options'] ) ) {
				$xml .= '      <options>' . "\n";
				foreach ( $q['options'] as $opt ) {
					$text    = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
					$correct = ( is_array( $opt ) && ! empty( $opt['correct'] ) ) ? 'true' : 'false';
					$xml    .= '        <option correct="' . $correct . '">' . $this->xml_escape( $text ) . '</option>' . "\n";
				}
				$xml .= '      </options>' . "\n";
			}

			if ( ! empty( $q['explanation'] ) ) {
				$xml .= '      <explanation>' . $this->xml_escape( $q['explanation'] ) . '</explanation>' . "\n";
			}

			$xml .= '    </question>' . "\n";
		}

		$xml .= '  </questions>' . "\n";
		$xml .= '</quiz>';

		return $xml;
	}

	/* =============================================
	   Format: CSV
	   ============================================= */

	/**
	 * Export to CSV format.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string CSV content.
	 */
	private function to_csv( $quiz_data, $questions ) {
		$stream = fopen( 'php://temp', 'r+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- in-memory stream, not a file.
		fputcsv( $stream, array( 'Question', 'Type', 'Points', 'Option A', 'Option B', 'Option C', 'Option D', 'Correct', 'Explanation', 'Hint' ) );

		foreach ( $questions as $q ) {
			$options = array( '', '', '', '' );
			$correct = '';

			if ( ! empty( $q['options'] ) ) {
				foreach ( $q['options'] as $oi => $opt ) {
					if ( $oi >= 4 ) {
						break;
					}
					$options[ $oi ] = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
					if ( is_array( $opt ) && ! empty( $opt['correct'] ) ) {
						$correct = chr( 65 + $oi );
					}
				}
			}

			fputcsv( $stream, array(
				$this->q_text( $q ),
				$q['type'] ?? 'multiple_choice',
				$q['points'] ?? 0,
				$options[0],
				$options[1],
				$options[2],
				$options[3],
				$correct,
				$q['explanation'] ?? '',
				$q['hint'] ?? '',
			) );
		}

		rewind( $stream );
		$content = stream_get_contents( $stream );
		fclose( $stream ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- in-memory stream.

		return $content;
	}

	/* =============================================
	   Format: Plain Text
	   ============================================= */

	/**
	 * Export to plain text.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string Text content.
	 */
	private function to_txt( $quiz_data, $questions ) {
		$title = isset( $quiz_data['quiz_title'] ) ? $quiz_data['quiz_title'] : 'Quiz';
		$out   = $title . "\n" . str_repeat( '=', strlen( $title ) ) . "\n\n";

		foreach ( $questions as $i => $q ) {
			$out .= ( $i + 1 ) . '. ' . $this->q_text( $q ) . "\n";

			if ( ! empty( $q['options'] ) ) {
				foreach ( $q['options'] as $oi => $opt ) {
					$text    = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
					$marker  = ( is_array( $opt ) && ! empty( $opt['correct'] ) ) ? '*' : ' ';
					$out    .= '   ' . chr( 65 + $oi ) . ')' . $marker . ' ' . $text . "\n";
				}
			}

			if ( ! empty( $q['model_answer'] ) ) {
				$out .= "   Model Answer: " . $q['model_answer'] . "\n";
			}

			if ( ! empty( $q['explanation'] ) ) {
				$out .= "   Explanation: " . $q['explanation'] . "\n";
			}

			$out .= "\n";
		}

		return $out;
	}

	/* =============================================
	   Format: GIFT (Moodle)
	   ============================================= */

	/**
	 * Export to Moodle GIFT format.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string GIFT text.
	 */
	private function to_gift( $quiz_data, $questions ) {
		$out = '';

		foreach ( $questions as $q ) {
			$type = strtolower( $q['type'] ?? 'multiple_choice' );
			$text = $this->gift_escape( $this->q_text( $q ) );

			if ( in_array( $type, array( 'essay', 'analytical_essay', 'comparative_analysis' ), true ) ) {
				$out .= '::' . mb_substr( $text, 0, 50 ) . '::' . $text . "{}\n\n";
				continue;
			}

			$out .= '::' . mb_substr( $text, 0, 50 ) . '::' . $text . "{\n";
			if ( ! empty( $q['options'] ) ) {
				foreach ( $q['options'] as $opt ) {
					$opt_text = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
					$correct  = ( is_array( $opt ) && ! empty( $opt['correct'] ) );
					$prefix   = $correct ? '=' : '~';
					$out     .= '  ' . $prefix . $this->gift_escape( $opt_text ) . "\n";
				}
			}
			$out .= "}\n\n";
		}

		return $out;
	}

	/* =============================================
	   Format: QTI 1.2
	   ============================================= */

	/**
	 * Export to QTI 1.2 format.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string QTI XML.
	 */
	private function to_qti( $quiz_data, $questions ) {
		$title = isset( $quiz_data['quiz_title'] ) ? $quiz_data['quiz_title'] : 'Quiz';

		$xml  = '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
		$xml .= '<questestinterop>' . "\n";
		$xml .= '  <assessment title="' . $this->xml_escape( $title ) . '">' . "\n";
		$xml .= '    <section>' . "\n";

		foreach ( $questions as $i => $q ) {
			$ident = 'q' . ( $i + 1 );
			$xml  .= '      <item ident="' . $ident . '">' . "\n";
			$xml  .= '        <presentation>' . "\n";
			$xml  .= '          <material><mattext>' . $this->xml_escape( $this->q_text( $q ) ) . '</mattext></material>' . "\n";

			if ( ! empty( $q['options'] ) ) {
				$xml .= '          <response_lid ident="response_' . $ident . '" rcardinality="Single">' . "\n";
				$xml .= '            <render_choice>' . "\n";
				foreach ( $q['options'] as $oi => $opt ) {
					$text = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
					$xml .= '              <response_label ident="' . $ident . '_' . $oi . '"><material><mattext>' . $this->xml_escape( $text ) . '</mattext></material></response_label>' . "\n";
				}
				$xml .= '            </render_choice>' . "\n";
				$xml .= '          </response_lid>' . "\n";
			}

			$xml .= '        </presentation>' . "\n";

			// Correct answer(s).
			if ( ! empty( $q['options'] ) ) {
				$xml .= '        <resprocessing>' . "\n";
				$xml .= '          <respcondition>' . "\n";
				$xml .= '            <conditionvar>' . "\n";
				foreach ( $q['options'] as $oi => $opt ) {
					if ( is_array( $opt ) && ! empty( $opt['correct'] ) ) {
						$xml .= '              <varequal respident="response_' . $ident . '">' . $ident . '_' . $oi . '</varequal>' . "\n";
					}
				}
				$xml .= '            </conditionvar>' . "\n";
				$xml .= '          </respcondition>' . "\n";
				$xml .= '        </resprocessing>' . "\n";
			}

			$xml .= '      </item>' . "\n";
		}

		$xml .= '    </section>' . "\n";
		$xml .= '  </assessment>' . "\n";
		$xml .= '</questestinterop>';

		return $xml;
	}

	/* =============================================
	   Format: Aiken
	   ============================================= */

	/**
	 * Export to Aiken format (simple MCQ text format).
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string Aiken text.
	 */
	private function to_aiken( $quiz_data, $questions ) {
		$out = '';

		foreach ( $questions as $q ) {
			// Aiken format supports only MCQ-type questions — skip essays.
			$type = strtolower( $q['type'] ?? 'multiple_choice' );
			if ( in_array( $type, array( 'essay', 'analytical_essay', 'comparative_analysis' ), true ) ) {
				continue;
			}
			if ( empty( $q['options'] ) ) {
				continue;
			}

			$out    .= $this->q_text( $q ) . "\n";
			$correct = 'A';

			foreach ( $q['options'] as $oi => $opt ) {
				$text = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
				$out .= chr( 65 + $oi ) . '. ' . $text . "\n";
				if ( is_array( $opt ) && ! empty( $opt['correct'] ) ) {
					$correct = chr( 65 + $oi );
				}
			}

			$out .= 'ANSWER: ' . $correct . "\n\n";
		}

		return $out;
	}

	/* =============================================
	   Format: LearnDash CSV
	   ============================================= */

	/**
	 * Export to LearnDash CSV import format.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string CSV content.
	 */
	private function to_learndash_csv( $quiz_data, $questions ) {
		$stream = fopen( 'php://temp', 'r+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- in-memory stream, not a file.
		fputcsv( $stream, array(
			'Question', 'Question Type', 'Points', 'Answer 1', 'Answer 2',
			'Answer 3', 'Answer 4', 'Correct Answer', 'Category', 'Hint', 'Explanation',
		) );

		$quiz_title = isset( $quiz_data['quiz_title'] ) ? $quiz_data['quiz_title'] : 'Quiz';

		foreach ( $questions as $q ) {
			$type = strtolower( $q['type'] ?? 'multiple_choice' );

			// Map to LearnDash question types.
			if ( in_array( $type, array( 'essay', 'analytical_essay', 'comparative_analysis' ), true ) ) {
				$ld_type = 'free_answer';
			} else {
				$ld_type = 'single';
			}

			$answers = array( '', '', '', '' );
			$correct = 1;

			if ( ! empty( $q['options'] ) ) {
				foreach ( $q['options'] as $oi => $opt ) {
					if ( $oi >= 4 ) {
						break;
					}
					$answers[ $oi ] = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
					if ( is_array( $opt ) && ! empty( $opt['correct'] ) ) {
						$correct = $oi + 1;
					}
				}
			}

			$category = '';
			if ( ! empty( $q['concept_tags'] ) && is_array( $q['concept_tags'] ) ) {
				$category = implode( ', ', $q['concept_tags'] );
			}
			if ( empty( $category ) ) {
				$category = $quiz_title;
			}

			fputcsv( $stream, array(
				$this->q_text( $q ),
				$ld_type,
				$q['points'] ?? 1,
				$answers[0],
				$answers[1],
				$answers[2],
				$answers[3],
				$correct,
				$category,
				$q['hint'] ?? '',
				$q['explanation'] ?? '',
			) );
		}

		rewind( $stream );
		$content = stream_get_contents( $stream );
		fclose( $stream ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- in-memory stream.

		return $content;
	}

	/* =============================================
	   Format: LearnDash XML (WpProQuiz)
	   ============================================= */

	/**
	 * Export to LearnDash XML (WpProQuiz import format).
	 *
	 * This format is compatible with LearnDash's native quiz import feature.
	 * Encoding MUST be lowercase 'utf-8' for LearnDash compatibility.
	 *
	 * @param array $quiz_data Quiz data.
	 * @param array $questions Questions.
	 * @return string XML content.
	 */
	private function to_learndash_xml( $quiz_data, $questions ) {
		$title       = isset( $quiz_data['quiz_title'] ) ? $quiz_data['quiz_title'] : 'Generated Quiz';
		$description = isset( $quiz_data['description'] ) ? $quiz_data['description'] : $title;

		$xml  = '<?xml version="1.0" encoding="utf-8"?>' . "\n";
		$xml .= '<wpProQuiz>' . "\n";

		// Header.
		$xml .= '  <header version="0.29" exportVersion="1" ld_version="4.24.0" LEARNDASH_SETTINGS_DB_VERSION="2.5" />' . "\n";

		// Data.
		$xml .= '  <data>' . "\n";
		$xml .= '    <quiz>' . "\n";

		// Quiz title and text.
		$xml .= '      <title titleHidden="true"><![CDATA[' . $title . ']]></title>' . "\n";
		$xml .= '      <text><![CDATA[' . $description . ']]></text>' . "\n";

		// Quiz configuration.
		$xml .= '      <btnRestartQuizHidden>false</btnRestartQuizHidden>' . "\n";
		$xml .= '      <btnViewQuestionHidden>true</btnViewQuestionHidden>' . "\n";
		$xml .= '      <questionRandom>false</questionRandom>' . "\n";
		$xml .= '      <answerRandom>false</answerRandom>' . "\n";
		$xml .= '      <timeLimit>0</timeLimit>' . "\n";
		$xml .= '      <showPoints>true</showPoints>' . "\n";
		$xml .= '      <numberedAnswer>false</numberedAnswer>' . "\n";
		$xml .= '      <hideAnswerMessageBox>true</hideAnswerMessageBox>' . "\n";
		$xml .= '      <disabledAnswerMark>true</disabledAnswerMark>' . "\n";
		$xml .= '      <showAverageResult>true</showAverageResult>' . "\n";
		$xml .= '      <prerequisite>false</prerequisite>' . "\n";
		$xml .= '      <showReviewQuestion>false</showReviewQuestion>' . "\n";
		$xml .= '      <quizSummaryHide>false</quizSummaryHide>' . "\n";
		$xml .= '      <skipQuestionDisabled>false</skipQuestionDisabled>' . "\n";
		$xml .= '      <emailNotification>0</emailNotification>' . "\n";
		$xml .= '      <userEmailNotification>false</userEmailNotification>' . "\n";
		$xml .= '      <showCategoryScore>true</showCategoryScore>' . "\n";
		$xml .= '      <hideResultCorrectQuestion>false</hideResultCorrectQuestion>' . "\n";
		$xml .= '      <hideResultQuizTime>false</hideResultQuizTime>' . "\n";
		$xml .= '      <hideResultPoints>false</hideResultPoints>' . "\n";
		$xml .= '      <autostart>true</autostart>' . "\n";
		$xml .= '      <forcingQuestionSolve>false</forcingQuestionSolve>' . "\n";
		$xml .= '      <hideQuestionPositionOverview>true</hideQuestionPositionOverview>' . "\n";
		$xml .= '      <hideQuestionNumbering>true</hideQuestionNumbering>' . "\n";
		$xml .= '      <sortCategories>false</sortCategories>' . "\n";
		$xml .= '      <showCategory>false</showCategory>' . "\n";
		$xml .= '      <quizModus questionsPerPage="0">2</quizModus>' . "\n";

		// Result text.
		$xml .= '      <resultText gradeEnabled="true">' . "\n";
		$xml .= '        <text prozent="0"><![CDATA[]]></text>' . "\n";
		$xml .= '      </resultText>' . "\n";

		// Statistics.
		$xml .= '      <statistic activated="false" ipLock="0" />' . "\n";

		// Forms.
		$xml .= '      <forms activated="false" position="0" />' . "\n";

		// Questions.
		$xml .= '      <questions>' . "\n";

		foreach ( $questions as $q ) {
			$xml .= $this->build_learndash_question_xml( $q );
		}

		$xml .= '      </questions>' . "\n";
		$xml .= '    </quiz>' . "\n";

		// Post metadata.
		$xml .= $this->build_learndash_post_xml( $title );

		$xml .= '  </data>' . "\n";
		$xml .= '</wpProQuiz>';

		return $xml;
	}

	/**
	 * Build a single question XML node for LearnDash export.
	 *
	 * @param array $q Question data.
	 * @return string XML fragment.
	 */
	private function build_learndash_question_xml( $q ) {
		$type       = strtolower( $q['type'] ?? 'multiple_choice' );
		$q_text     = $this->q_text( $q );
		$q_title    = mb_substr( $q_text, 0, 50 );
		$points     = (int) ( $q['points'] ?? 1 );
		$explanation = $q['explanation'] ?? '';
		$hint       = $q['hint'] ?? '';

		// Determine answer type.
		if ( in_array( $type, array( 'essay', 'analytical_essay', 'comparative_analysis' ), true ) ) {
			$answer_type = 'essay';
		} elseif ( 'true_false' === $type ) {
			$answer_type = 'single';
		} else {
			// Check for multiple correct answers.
			$correct_count = 0;
			if ( ! empty( $q['options'] ) ) {
				foreach ( $q['options'] as $opt ) {
					if ( is_array( $opt ) && ! empty( $opt['correct'] ) ) {
						$correct_count++;
					}
				}
			}
			$answer_type = $correct_count > 1 ? 'multiple' : 'single';
		}

		$xml  = '        <question answerType="' . $answer_type . '">' . "\n";
		$xml .= '          <title><![CDATA[' . $q_title . ']]></title>' . "\n";
		$xml .= '          <points>' . $points . '</points>' . "\n";
		$xml .= '          <questionText><![CDATA[' . $q_text . ']]></questionText>' . "\n";
		$xml .= '          <correctMsg><![CDATA[' . $explanation . ']]></correctMsg>' . "\n";
		$xml .= '          <incorrectMsg><![CDATA[]]></incorrectMsg>' . "\n";

		$tip_enabled = ! empty( $hint ) ? 'true' : 'false';
		$xml .= '          <tipMsg enabled="' . $tip_enabled . '"><![CDATA[' . $hint . ']]></tipMsg>' . "\n";

		$category = '';
		if ( ! empty( $q['concept_tags'] ) && is_array( $q['concept_tags'] ) ) {
			$category = implode( ', ', $q['concept_tags'] );
		}
		$xml .= '          <category><![CDATA[' . $category . ']]></category>' . "\n";

		$xml .= '          <correctSameText>false</correctSameText>' . "\n";
		$xml .= '          <showPointsInBox>false</showPointsInBox>' . "\n";
		$xml .= '          <answerPointsActivated>false</answerPointsActivated>' . "\n";
		$xml .= '          <answerPointsDiffModusActivated>false</answerPointsDiffModusActivated>' . "\n";
		$xml .= '          <disableCorrect>false</disableCorrect>' . "\n";

		// Answers.
		$xml .= '          <answers>' . "\n";

		if ( 'essay' === $answer_type ) {
			$xml .= '            <answer points="0" correct="false" gradingProgression="not-graded-none" gradedType="text">' . "\n";
			$xml .= '              <answerText html="false"><![CDATA[]]></answerText>' . "\n";
			$xml .= '              <stortText html="false"><![CDATA[]]></stortText>' . "\n";
			$xml .= '            </answer>' . "\n";
		} elseif ( ! empty( $q['options'] ) ) {
			foreach ( $q['options'] as $opt ) {
				$opt_text   = is_string( $opt ) ? $opt : ( $opt['text'] ?? '' );
				$is_correct = ( is_array( $opt ) && ! empty( $opt['correct'] ) );

				if ( 'multiple' === $answer_type ) {
					$opt_points = '0';
				} else {
					$opt_points = $is_correct ? '0' : '1';
				}

				$correct_str = $is_correct ? 'true' : 'false';

				$xml .= '            <answer points="' . $opt_points . '" correct="' . $correct_str . '">' . "\n";
				$xml .= '              <answerText html="false"><![CDATA[' . $opt_text . ']]></answerText>' . "\n";
				$xml .= '              <stortText html="false"><![CDATA[]]></stortText>' . "\n";
				$xml .= '            </answer>' . "\n";
			}
		}

		$xml .= '          </answers>' . "\n";
		$xml .= '        </question>' . "\n";

		return $xml;
	}

	/**
	 * Build the LearnDash post metadata XML section.
	 *
	 * @param string $title Quiz title.
	 * @return string XML fragment.
	 */
	private function build_learndash_post_xml( $title ) {
		$sfwd_quiz_meta = wp_json_encode( array(
			array(
				'sfwd-quiz_quiz_pro'              => 1,
				'sfwd-quiz_course'                => 0,
				'sfwd-quiz_lesson'                => 0,
				'sfwd-quiz_passingpercentage'     => '0',
				'sfwd-quiz_timeLimit'             => 0,
				'sfwd-quiz_showPoints'            => true,
				'sfwd-quiz_showCategory'          => false,
				'sfwd-quiz_answerRandom'          => false,
				'sfwd-quiz_questionRandom'        => '',
				'sfwd-quiz_quizModus'             => 2,
				'sfwd-quiz_autostart'             => true,
				'sfwd-quiz_forcingQuestionSolve'  => false,
			),
		) );

		$xml  = '    <post>' . "\n";
		$xml .= '      <post_title><![CDATA[' . $title . ']]></post_title>' . "\n";
		$xml .= '      <post_content><![CDATA[]]></post_content>' . "\n";

		// _timeLimitCookie.
		$xml .= '      <post_meta>' . "\n";
		$xml .= '        <meta_key><![CDATA[_timeLimitCookie]]></meta_key>' . "\n";
		$xml .= '        <meta_value><![CDATA[["0"]]]></meta_value>' . "\n";
		$xml .= '      </post_meta>' . "\n";

		// _viewProfileStatistics.
		$xml .= '      <post_meta>' . "\n";
		$xml .= '        <meta_key><![CDATA[_viewProfileStatistics]]></meta_key>' . "\n";
		$xml .= '        <meta_value><![CDATA[["1"]]]></meta_value>' . "\n";
		$xml .= '      </post_meta>' . "\n";

		// _sfwd-quiz meta.
		$xml .= '      <post_meta>' . "\n";
		$xml .= '        <meta_key><![CDATA[_sfwd-quiz]]></meta_key>' . "\n";
		$xml .= '        <meta_value><![CDATA[' . $sfwd_quiz_meta . ']]></meta_value>' . "\n";
		$xml .= '      </post_meta>' . "\n";

		$xml .= '    </post>' . "\n";

		return $xml;
	}

	/* =============================================
	   Escaping helpers
	   ============================================= */

	/**
	 * Escape a string for use inside XML text nodes.
	 *
	 * @param string $str Input string.
	 * @return string Escaped string.
	 */
	private function xml_escape( $str ) {
		return htmlspecialchars( (string) $str, ENT_XML1 | ENT_QUOTES, 'UTF-8' );
	}

	/**
	 * Escape a string for use inside GIFT format text.
	 *
	 * @param string $str Input string.
	 * @return string Escaped string.
	 */
	private function gift_escape( $str ) {
		return str_replace(
			array( '~', '=', '#', '{', '}', ':' ),
			array( '\\~', '\\=', '\\#', '\\{', '\\}', '\\:' ),
			(string) $str
		);
	}
}
