<?php
/**
 * WPFAQBlock AI Generator
 *
 * AJAX handler: accepts a topic, calls OpenAI or Google Gemini API,
 * returns generated question + answer pairs.
 *
 * @package WPFAQBlock
 * @since   2.0.0
 */

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

/**
 * Class WPFAQBlock_AI
 */
class WPFAQBlock_AI {

	/** OpenAI endpoint. */
	const OPENAI_URL = 'https://api.openai.com/v1/chat/completions';

	/** Nonce action used by the single post editor AI panel. */
	const EDITOR_NONCE_ACTION = 'wpfaqblock_editor_ai_nonce';

	/** Maximum source content characters sent to AI from the editor. */
	const EDITOR_CONTENT_LIMIT = 14000;

	/** Maximum number of FAQs saved from the editor AI panel. */
	const EDITOR_MAX_FAQS = 15;

	/**
	 * Register AJAX actions and editor hooks.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function register(): void {
		add_action( 'wp_ajax_wpfaqblock_generate_ai', array( $this, 'handle_generate' ) );
		add_action( 'wp_ajax_wpfaqblock_generate_editor_faqs', array( $this, 'handle_generate_editor_faqs' ) );
		add_action( 'wp_ajax_wpfaqblock_save_editor_ai_faqs', array( $this, 'handle_save_editor_faqs' ) );
		add_action( 'add_meta_boxes', array( $this, 'add_editor_meta_box' ), 10, 2 );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_editor_assets' ) );
	}

	/**
	 * Handle AJAX request.
	 */
	public function handle_generate(): void {
		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'wpfaqblock_builder_nonce' ) || ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed.', 'wpfaqblock' ) ), 403 );
		}

		$openai_key = WPFAQBlock_Settings::get_api_key();
		$gemini_key = WPFAQBlock_Settings::get_gemini_key();

		if ( empty( $openai_key ) && empty( $gemini_key ) ) {
			wp_send_json_error(
				array(
					'message'     => __( 'API key is not set. Please add your OpenAI or Google Gemini API key.', 'wpfaqblock' ),
					'missing_key' => true,
				)
			);
		}

		$topic = isset( $_POST['topic'] ) ? sanitize_text_field( wp_unslash( $_POST['topic'] ) ) : '';
		$count = isset( $_POST['count'] ) ? absint( $_POST['count'] ) : 5;
		$tone = isset( $_POST['tone'] ) ? sanitize_key( wp_unslash( $_POST['tone'] ) ) : 'professional';
		$audience = isset( $_POST['audience'] ) ? sanitize_key( wp_unslash( $_POST['audience'] ) ) : 'general';
		if ( $count < 1 || $count > 20 ) {
			wp_send_json_error( array( 'message' => __( 'Please enter a number between 1 and 20.', 'wpfaqblock' ) ) );
		}

		if ( empty( $topic ) ) {
			wp_send_json_error( array( 'message' => __( 'Please enter a topic.', 'wpfaqblock' ) ) );
		}

		$prompt = $this->build_topic_prompt( $topic, $count, $tone, $audience );
		$result = $this->request_faqs_with_fallback( $prompt, 20, $gemini_key, $openai_key );

		$this->send_ai_result( $result );
	}

	/**
	 * Add the editor-only AI FAQ generator meta box.
	 *
	 * @since 2.0.0
	 *
	 * @param string $post_type Current post type.
	 * @param mixed  $post      Current post object.
	 * @return void
	 */
	public function add_editor_meta_box( string $post_type, $post ): void {
		if ( ! $post instanceof WP_Post ) {
			return;
		}

		if ( ! $this->is_supported_editor_post_type( $post_type ) || ! current_user_can( 'edit_post', $post->ID ) ) {
			return;
		}

		add_meta_box(
			'wpfaqblock-editor-ai',
			__( 'Generate FAQs', 'wpfaqblock' ),
			array( $this, 'render_editor_meta_box' ),
			$post_type,
			'side',
			'default'
		);
	}

	/**
	 * Render the editor-only AI FAQ generator meta box.
	 *
	 * @since 2.0.0
	 *
	 * @param WP_Post $post Current post.
	 * @return void
	 */
	public function render_editor_meta_box( WP_Post $post ): void {
		$terms = get_terms(
			array(
				'taxonomy'   => WPFAQBlock_Taxonomy::SLUG,
				'hide_empty' => false,
				'orderby'    => 'name',
				'order'      => 'ASC',
			)
		);
		?>
		<div
			id="wpfaqblock-editor-ai-box"
			class="wpfaqblock-editor-ai-box"
			data-post-id="<?php echo esc_attr( (string) $post->ID ); ?>"
			data-post-type="<?php echo esc_attr( $post->post_type ); ?>"
		>
			<p class="wpfaqblock-editor-ai-box__intro">
				<?php esc_html_e( 'Generate FAQ suggestions from this editor content, then save the selected items to WPFAQBlock.', 'wpfaqblock' ); ?>
			</p>

			<label class="wpfaqblock-editor-ai-box__label" for="wpfaqblock-editor-ai-group">
				<?php esc_html_e( 'FAQ Category', 'wpfaqblock' ); ?>
			</label>
			<select id="wpfaqblock-editor-ai-group" class="widefat">
				<option value="0"><?php esc_html_e( 'No Category', 'wpfaqblock' ); ?></option>
				<?php if ( ! is_wp_error( $terms ) ) : ?>
					<?php foreach ( $terms as $term ) : ?>
						<option value="<?php echo esc_attr( (string) $term->term_id ); ?>">
							<?php echo esc_html( $term->name ); ?>
						</option>
					<?php endforeach; ?>
				<?php endif; ?>
			</select>

			<button type="button" class="button button-primary wpfaqblock-editor-ai-box__generate" id="wpfaqblock-editor-ai-generate">
				<?php esc_html_e( 'Generate FAQs', 'wpfaqblock' ); ?>
			</button>

			<div id="wpfaqblock-editor-ai-notice" class="wpfaqblock-editor-ai-notice" hidden></div>

			<div id="wpfaqblock-editor-ai-results" class="wpfaqblock-editor-ai-results" hidden>
				<div id="wpfaqblock-editor-ai-list" class="wpfaqblock-editor-ai-list"></div>
				<button type="button" class="button button-secondary wpfaqblock-editor-ai-box__save" id="wpfaqblock-editor-ai-save">
					<?php esc_html_e( 'Save Selected FAQs', 'wpfaqblock' ); ?>
				</button>
			</div>
		</div>
		<?php
	}

	/**
	 * Enqueue editor AI assets only on single post edit screens.
	 *
	 * @since 2.0.0
	 *
	 * @param string $hook_suffix Current admin page hook.
	 * @return void
	 */
	public function enqueue_editor_assets( string $hook_suffix ): void {
		if ( 'post.php' !== $hook_suffix && 'post-new.php' !== $hook_suffix ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || empty( $screen->post_type ) || ! $this->is_supported_editor_post_type( $screen->post_type ) ) {
			return;
		}

		wp_enqueue_style(
			'wpfaqblock-editor-ai',
			WPFAQBLOCK_URL . 'assets/css/editor-ai.css',
			array(),
			file_exists( WPFAQBLOCK_DIR . 'assets/css/editor-ai.css' ) ? filemtime( WPFAQBLOCK_DIR . 'assets/css/editor-ai.css' ) : WPFAQBLOCK_VERSION
		);

		wp_enqueue_script(
			'wpfaqblock-editor-ai',
			WPFAQBLOCK_URL . 'assets/js/editor-ai.js',
			array(),
			file_exists( WPFAQBLOCK_DIR . 'assets/js/editor-ai.js' ) ? filemtime( WPFAQBLOCK_DIR . 'assets/js/editor-ai.js' ) : WPFAQBLOCK_VERSION,
			true
		);

		wp_localize_script(
			'wpfaqblock-editor-ai',
			'wpfaqblockEditorAI',
			array(
				'ajaxUrl'     => admin_url( 'admin-ajax.php' ),
				'nonce'       => wp_create_nonce( self::EDITOR_NONCE_ACTION ),
				'postType'    => $screen->post_type,
				'settingsUrl' => admin_url( 'admin.php?page=' . WPFAQBlock_Settings::PAGE_SLUG ),
				'i18n'        => array(
					'emptyContent'  => __( 'Add content to this editor before generating FAQs.', 'wpfaqblock' ),
					'generating'    => __( 'Generating...', 'wpfaqblock' ),
					'generate'      => __( 'Generate FAQs', 'wpfaqblock' ),
					'saving'        => __( 'Saving...', 'wpfaqblock' ),
					'save'          => __( 'Save Selected FAQs', 'wpfaqblock' ),
					'selectFaqs'    => __( 'Select at least one FAQ to save.', 'wpfaqblock' ),
					'requestFailed' => __( 'AI request failed. Please try again.', 'wpfaqblock' ),
					'saveFailed'    => __( 'Could not save FAQs. Please try again.', 'wpfaqblock' ),
					'missingKey'    => __( 'API key is not set. Add an OpenAI or Gemini API key in WPFAQBlock Settings.', 'wpfaqblock' ),
					'settings'      => __( 'Open Settings', 'wpfaqblock' ),
				),
			)
		);
	}

	/**
	 * Generate FAQs from the currently edited post content.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function handle_generate_editor_faqs(): void {
		check_ajax_referer( self::EDITOR_NONCE_ACTION, 'nonce' );

		$source_check = $this->validate_editor_source_request();
		if ( is_wp_error( $source_check ) ) {
			wp_send_json_error( array( 'message' => $source_check->get_error_message() ), 403 );
		}

		$openai_key = WPFAQBlock_Settings::get_api_key();
		$gemini_key = WPFAQBlock_Settings::get_gemini_key();

		if ( empty( $openai_key ) && empty( $gemini_key ) ) {
			wp_send_json_error(
				array(
					'message'     => __( 'API key is not set. Please add your OpenAI or Google Gemini API key.', 'wpfaqblock' ),
					'missing_key' => true,
				)
			);
		}

		$title = isset( $_POST['source_title'] ) && is_scalar( $_POST['source_title'] )
			? sanitize_text_field( wp_unslash( (string) $_POST['source_title'] ) )
			: '';
		$content = isset( $_POST['source_content'] ) && is_scalar( $_POST['source_content'] )
			? wp_unslash( (string) $_POST['source_content'] )
			: '';

		$source = $this->prepare_source_content( $title, $content );
		if ( empty( $source['content'] ) || strlen( $source['content'] ) < 80 ) {
			wp_send_json_error( array( 'message' => __( 'Add more content before generating FAQs.', 'wpfaqblock' ) ) );
		}

		$count = $this->determine_editor_faq_count( $source['content'] );
		$prompt = $this->build_content_prompt( $source['content'], $count );
		$result = $this->request_faqs_with_fallback( $prompt, self::EDITOR_MAX_FAQS, $gemini_key, $openai_key );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'faqs'      => $result,
				'count'     => count( $result ),
				'truncated' => $source['truncated'],
			)
		);
	}

	/**
	 * Save reviewed AI-generated FAQs into the existing FAQ CPT.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function handle_save_editor_faqs(): void {
		check_ajax_referer( self::EDITOR_NONCE_ACTION, 'nonce' );

		$source_check = $this->validate_editor_source_request();
		if ( is_wp_error( $source_check ) ) {
			wp_send_json_error( array( 'message' => $source_check->get_error_message() ), 403 );
		}

		$faq_type = get_post_type_object( WPFAQBlock_CPT::SLUG );
		if ( ! $faq_type || ! current_user_can( $faq_type->cap->publish_posts ) ) {
			wp_send_json_error( array( 'message' => __( 'You do not have permission to save FAQs.', 'wpfaqblock' ) ), 403 );
		}

		$raw_faqs = isset( $_POST['faqs'] ) && is_scalar( $_POST['faqs'] ) ? wp_unslash( (string) $_POST['faqs'] ) : '';
		$items = json_decode( $raw_faqs, true );
		if ( ! is_array( $items ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid FAQ data.', 'wpfaqblock' ) ) );
		}

		$group_id = isset( $_POST['group_id'] ) ? absint( $_POST['group_id'] ) : 0;
		if ( $group_id > 0 && ! term_exists( $group_id, WPFAQBlock_Taxonomy::SLUG ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid FAQ category.', 'wpfaqblock' ) ) );
		}

		$created = array();
		$skipped = 0;
		$items = array_slice( $items, 0, self::EDITOR_MAX_FAQS );

		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				$skipped++;
				continue;
			}

			$question = isset( $item['question'] ) && is_scalar( $item['question'] ) ? sanitize_text_field( (string) $item['question'] ) : '';
			$answer = isset( $item['answer'] ) && is_scalar( $item['answer'] ) ? wp_kses_post( (string) $item['answer'] ) : '';

			if ( '' === $question || '' === $answer ) {
				$skipped++;
				continue;
			}

			$post_id = wp_insert_post(
				array(
					'post_type'    => WPFAQBlock_CPT::SLUG,
					'post_title'   => $question,
					'post_content' => $answer,
					'post_status'  => 'publish',
					'post_author'  => get_current_user_id(),
					'menu_order'   => 0,
				),
				true
			);

			if ( is_wp_error( $post_id ) ) {
				$skipped++;
				continue;
			}

			if ( $group_id > 0 ) {
				wp_set_object_terms( $post_id, array( $group_id ), WPFAQBlock_Taxonomy::SLUG );
			}

			$created[] = array(
				'id'       => (int) $post_id,
				'question' => $question,
			);
		}

		if ( empty( $created ) ) {
			wp_send_json_error( array( 'message' => __( 'No valid FAQs were selected.', 'wpfaqblock' ) ) );
		}

		wp_send_json_success(
			array(
				'message' => sprintf(
					/* translators: 1: saved count, 2: skipped count */
					__( 'Saved %1$d FAQ(s). Skipped %2$d item(s).', 'wpfaqblock' ),
					count( $created ),
					$skipped
				),
				'created' => $created,
				'skipped' => $skipped,
			)
		);
	}

	/**
	 * Request FAQs using Gemini first, then fall back to OpenAI when available.
	 *
	 * @since 2.0.0
	 *
	 * @param string $prompt     Prompt text.
	 * @param int    $max        Maximum FAQs to return.
	 * @param string $gemini_key Gemini API key.
	 * @param string $openai_key OpenAI API key.
	 * @return array|WP_Error
	 */
	private function request_faqs_with_fallback( string $prompt, int $max, string $gemini_key, string $openai_key ) {
		if ( ! empty( $gemini_key ) ) {
			$gemini_result = $this->request_gemini_faqs( $gemini_key, $prompt, $max );
			if ( ! is_wp_error( $gemini_result ) ) {
				return $gemini_result;
			}

			if ( ! empty( $openai_key ) ) {
				$openai_result = $this->request_openai_faqs( $openai_key, $prompt, $max );
				if ( ! is_wp_error( $openai_result ) ) {
					return $openai_result;
				}

				return new WP_Error(
					'wpfaqblock_ai_fallback_failed',
					sprintf(
						/* translators: 1: Gemini error message, 2: OpenAI error message */
						__( 'Gemini failed: %1$s OpenAI fallback also failed: %2$s', 'wpfaqblock' ),
						$gemini_result->get_error_message(),
						$openai_result->get_error_message()
					)
				);
			}

			return $this->normalize_gemini_error( $gemini_result );
		}

		return $this->request_openai_faqs( $openai_key, $prompt, $max );
	}

	/**
	 * Build the existing builder topic prompt.
	 *
	 * @since 2.0.0
	 *
	 * @param string $topic    Topic supplied by the builder.
	 * @param int    $count    Number of FAQs requested.
	 * @param string $tone     Tone key.
	 * @param string $audience Audience key.
	 * @return string
	 */
	private function build_topic_prompt( string $topic, int $count, string $tone, string $audience ): string {
		return sprintf(
			'Generate %1$d FAQ items about "%2$s" for %3$s using a %4$s tone. Return JSON only as {"faqs":[{"question":"...","answer":"..."}]}. Each item must have "question" and "answer".',
			$count,
			$topic,
			$this->get_audience_label( $audience ),
			$this->get_tone_label( $tone )
		);
	}

	/**
	 * Build the post-content prompt requested for editor generation.
	 *
	 * @since 2.0.0
	 *
	 * @param string $content Prepared source content.
	 * @param int    $count   Number of FAQs to generate.
	 * @return string
	 */
	private function build_content_prompt( string $content, int $count ): string {
		return sprintf(
			"Generate FAQs based only on the supplied content.\n\nRequirements:\n* Generate between 5 and 15 FAQs depending on content length.\n* For this content, aim for %1\$d FAQs.\n* Questions should reflect what real users may search for.\n* Answers must be concise and accurate.\n* Do not invent information.\n* Do not create a FAQ unless the answer is explicitly supported by the supplied content.\n* If the supplied content cannot support enough unique accurate FAQs, return fewer rather than inventing.\n* Avoid duplicate questions.\n* Use clear language.\n* Return JSON only.\n\nExpected response:\n{\n  \"faqs\": [\n    {\n      \"question\": \"What is WPFAQBlock?\",\n      \"answer\": \"WPFAQBlock is a WordPress plugin for displaying FAQs using multiple layouts.\"\n    }\n  ]\n}\n\nSupplied content:\n\"\"\"\n%2\$s\n\"\"\"",
			$count,
			$content
		);
	}

	/**
	 * Request FAQs using Google Gemini.
	 *
	 * @since 2.0.0
	 *
	 * @param string $api_key API key.
	 * @param string $prompt  Prompt text.
	 * @param int    $max     Maximum FAQs to return.
	 * @return array|WP_Error
	 */
	private function request_gemini_faqs( string $api_key, string $prompt, int $max ) {
		$url = add_query_arg(
			'key',
			$api_key,
			'https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent'
		);

		$response = wp_remote_post(
			$url,
			array(
				'timeout' => 35,
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode(
					array(
						'contents' => array(
							array(
								'parts' => array(
									array( 'text' => $prompt ),
								),
							),
						),
					)
				),
			)
		);

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

		$response_code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( $response_code >= 400 && empty( $body['error'] ) ) {
			return new WP_Error(
				'wpfaqblock_gemini_http_error',
				sprintf(
					/* translators: %d: HTTP response code */
					__( 'Gemini request failed with HTTP status %d.', 'wpfaqblock' ),
					$response_code
				)
			);
		}

		if ( isset( $body['error'] ) ) {
			return new WP_Error(
				'wpfaqblock_gemini_error',
				sprintf(
					/* translators: %s: Gemini error message */
					__( 'Gemini error: %s', 'wpfaqblock' ),
					isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'Quota exceeded or API error.', 'wpfaqblock' )
				)
			);
		}

		$raw_text = isset( $body['candidates'][0]['content']['parts'][0]['text'] ) ? (string) $body['candidates'][0]['content']['parts'][0]['text'] : '';
		return $this->parse_ai_faq_response( $raw_text, $max );
	}

	/**
	 * Request FAQs using OpenAI.
	 *
	 * @since 2.0.0
	 *
	 * @param string $api_key API key.
	 * @param string $prompt  Prompt text.
	 * @param int    $max     Maximum FAQs to return.
	 * @return array|WP_Error
	 */
	private function request_openai_faqs( string $api_key, string $prompt, int $max ) {
		$response = wp_remote_post(
			self::OPENAI_URL,
			array(
				'timeout' => 35,
				'headers' => array(
					'Authorization' => 'Bearer ' . $api_key,
					'Content-Type'  => 'application/json',
				),
				'body'    => wp_json_encode(
					array(
						'model'           => 'gpt-3.5-turbo',
						'response_format' => array( 'type' => 'json_object' ),
						'messages'        => array(
							array(
								'role'    => 'user',
								'content' => $prompt,
							),
						),
					)
				),
			)
		);

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

		$response_code = (int) wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );
		if ( $response_code >= 400 && empty( $body['error'] ) ) {
			return new WP_Error(
				'wpfaqblock_openai_http_error',
				sprintf(
					/* translators: %d: HTTP response code */
					__( 'OpenAI request failed with HTTP status %d.', 'wpfaqblock' ),
					$response_code
				)
			);
		}

		if ( isset( $body['error'] ) ) {
			return new WP_Error(
				'wpfaqblock_openai_error',
				isset( $body['error']['message'] ) ? $body['error']['message'] : __( 'OpenAI error.', 'wpfaqblock' )
			);
		}

		$raw_text = isset( $body['choices'][0]['message']['content'] ) ? (string) $body['choices'][0]['message']['content'] : '';
		return $this->parse_ai_faq_response( $raw_text, $max );
	}

	/**
	 * Parse and sanitize AI response.
	 *
	 * Accepts both {"faqs": [...]} and the legacy [...] format.
	 *
	 * @since 2.0.0
	 *
	 * @param string $raw_text Raw AI text.
	 * @param int    $max      Maximum FAQs to return.
	 * @return array|WP_Error
	 */
	private function parse_ai_faq_response( string $raw_text, int $max ) {
		$raw_text = preg_replace( '/^```(?:json)?\s*/i', '', trim( $raw_text ) );
		$raw_text = preg_replace( '/\s*```$/', '', $raw_text );
		$faqs = json_decode( $raw_text, true );

		if ( ! is_array( $faqs ) && preg_match( '/(\{.*\}|\[.*\])/s', $raw_text, $matches ) ) {
			$faqs = json_decode( $matches[1], true );
		}

		if ( isset( $faqs['faqs'] ) && is_array( $faqs['faqs'] ) ) {
			$faqs = $faqs['faqs'];
		}

		if ( ! is_array( $faqs ) ) {
			return new WP_Error( 'wpfaqblock_invalid_ai_output', __( 'Invalid AI output.', 'wpfaqblock' ) );
		}

		$clean = array();
		$seen = array();

		foreach ( $faqs as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$question = isset( $item['question'] ) && is_scalar( $item['question'] ) ? sanitize_text_field( (string) $item['question'] ) : '';
			$answer = isset( $item['answer'] ) && is_scalar( $item['answer'] ) ? wp_kses_post( (string) $item['answer'] ) : '';
			$key = strtolower( preg_replace( '/\s+/', ' ', $question ) );

			if ( '' === $question || '' === $answer || isset( $seen[ $key ] ) ) {
				continue;
			}

			$seen[ $key ] = true;
			$clean[] = array(
				'question' => $question,
				'answer'   => $answer,
			);

			if ( count( $clean ) >= $max ) {
				break;
			}
		}

		if ( empty( $clean ) ) {
			return new WP_Error( 'wpfaqblock_empty_ai_output', __( 'AI did not return any usable FAQs.', 'wpfaqblock' ) );
		}

		return $clean;
	}

	/**
	 * Send a provider result as the existing builder AJAX response.
	 *
	 * @since 2.0.0
	 *
	 * @param array|WP_Error $result Result from provider.
	 * @return void
	 */
	private function send_ai_result( $result ): void {
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success( array( 'faqs' => $result ) );
	}

	/**
	 * Make common Gemini capacity failures clearer for users without fallback.
	 *
	 * @since 2.0.0
	 *
	 * @param WP_Error $error Original Gemini error.
	 * @return WP_Error
	 */
	private function normalize_gemini_error( WP_Error $error ): WP_Error {
		$message = $error->get_error_message();
		$lower_message = strtolower( $message );

		if (
			false !== strpos( $lower_message, 'high demand' )
			|| false !== strpos( $lower_message, 'try again later' )
			|| false !== strpos( $lower_message, 'overloaded' )
			|| false !== strpos( $lower_message, 'capacity' )
		) {
			return new WP_Error(
				'wpfaqblock_gemini_temporarily_busy',
				__( 'Gemini is temporarily busy. Please try again in a few minutes, or add an OpenAI API key in WPFAQBlock Settings to allow automatic fallback.', 'wpfaqblock' )
			);
		}

		return $error;
	}

	/**
	 * Validate the editor source request and user capability.
	 *
	 * @since 2.0.0
	 * @return true|WP_Error
	 */
	private function validate_editor_source_request() {
		$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
		$post_type = isset( $_POST['post_type'] ) && is_scalar( $_POST['post_type'] ) ? sanitize_key( wp_unslash( (string) $_POST['post_type'] ) ) : '';

		if ( $post_id > 0 ) {
			$post = get_post( $post_id );
			if ( ! $post instanceof WP_Post || ! $this->is_supported_editor_post_type( $post->post_type ) ) {
				return new WP_Error( 'wpfaqblock_invalid_post', __( 'Invalid source post.', 'wpfaqblock' ) );
			}

			if ( ! current_user_can( 'edit_post', $post_id ) ) {
				return new WP_Error( 'wpfaqblock_cannot_edit_post', __( 'You do not have permission to edit this content.', 'wpfaqblock' ) );
			}

			return true;
		}

		if ( ! $this->is_supported_editor_post_type( $post_type ) ) {
			return new WP_Error( 'wpfaqblock_invalid_post_type', __( 'Invalid source post type.', 'wpfaqblock' ) );
		}

		$post_type_object = get_post_type_object( $post_type );
		if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->edit_posts ) ) {
			return new WP_Error( 'wpfaqblock_cannot_edit_post_type', __( 'You do not have permission to edit this content type.', 'wpfaqblock' ) );
		}

		return true;
	}

	/**
	 * Determine if the editor AI panel should be available for a post type.
	 *
	 * @since 2.0.0
	 *
	 * @param string $post_type Post type.
	 * @return bool
	 */
	private function is_supported_editor_post_type( string $post_type ): bool {
		$excluded_post_types = array(
			WPFAQBlock_CPT::SLUG,
			'attachment',
			'shop_order',
			'shop_order_refund',
		);

		if ( '' === $post_type || in_array( $post_type, $excluded_post_types, true ) ) {
			return false;
		}

		$post_type_object = get_post_type_object( $post_type );
		return $post_type_object && $post_type_object->show_ui;
	}

	/**
	 * Prepare title/content for the AI prompt and truncate large content.
	 *
	 * @since 2.0.0
	 *
	 * @param string $title   Current editor title.
	 * @param string $content Current editor content.
	 * @return array
	 */
	private function prepare_source_content( string $title, string $content ): array {
		$content = strip_shortcodes( $content );
		$content = wp_kses_post( $content );
		$content = wp_strip_all_tags( $content, true );
		$content = html_entity_decode( $content, ENT_QUOTES, get_bloginfo( 'charset' ) );
		$content = preg_replace( '/\s+/', ' ', $content );
		$source = trim( $title . "\n\n" . $content );
		$truncated = false;

		if ( strlen( $source ) > self::EDITOR_CONTENT_LIMIT ) {
			$source = substr( $source, 0, self::EDITOR_CONTENT_LIMIT );
			$truncated = true;
		}

		return array(
			'content'   => trim( $source ),
			'truncated' => $truncated,
		);
	}

	/**
	 * Determine FAQ count from source content length.
	 *
	 * @since 2.0.0
	 *
	 * @param string $content Prepared source content.
	 * @return int
	 */
	private function determine_editor_faq_count( string $content ): int {
		$words = preg_split( '/\s+/', trim( $content ) );
		$word_count = is_array( $words ) ? count( array_filter( $words ) ) : 0;

		if ( $word_count > 2200 ) {
			return 15;
		}

		if ( $word_count > 1400 ) {
			return 12;
		}

		if ( $word_count > 800 ) {
			return 10;
		}

		if ( $word_count > 350 ) {
			return 8;
		}

		return 5;
	}

	/**
	 * Get a safe tone label for prompt context.
	 */
	private function get_tone_label( string $tone ): string {
		$tones = array(
			'professional' => 'professional',
			'friendly'     => 'friendly',
			'humorous'     => 'humorous',
		);

		return $tones[ $tone ] ?? $tones['professional'];
	}

	/**
	 * Get a safe audience label for prompt context.
	 */
	private function get_audience_label( string $audience ): string {
		$audiences = array(
			'general'   => 'a general audience',
			'customers' => 'existing customers',
			'technical' => 'technical experts',
			'buyers'    => 'potential buyers',
		);

		return $audiences[ $audience ] ?? $audiences['general'];
	}
}
