<?php
/**
 * Google Gemini AI provider.
 *
 * @package MasterQuiz
 * @since   1.0.0
 */

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

/**
 * Class MasterQuiz_Gemini_Provider
 *
 * Implements AI calls to the Google Gemini API using wp_remote_post().
 * Supports structured JSON output via responseSchema, with fallback to
 * unstructured prompting when the model returns 400.
 *
 * @since 1.0.0
 */
class MasterQuiz_Gemini_Provider extends MasterQuiz_Ai_Provider {

	/**
	 * Gemini API base URLs.
	 * v1beta first — required for responseSchema / thinkingConfig fields.
	 *
	 * @var array
	 */
	private $base_urls = array(
		'https://generativelanguage.googleapis.com/v1beta/models/',
		'https://generativelanguage.googleapis.com/v1/models/',
	);

	/**
	 * Whether to disable thinking for speed (set by provider chain for fast mode).
	 *
	 * @var bool
	 */
	private $disable_thinking = false;

	/**
	 * Cached model list for this request.
	 *
	 * @var string[]|null
	 */
	private $resolved_models = null;

	/**
	 * Hardcoded Gemini 3.x IDs for new Google projects.
	 * Do not include 1.5 / 2.0 / 2.5 — new keys often reject those.
	 *
	 * @var string[]
	 */
	private static $preferred_models = array(
		'gemini-3.5-flash',
		'gemini-3.5-flash-lite',
		'gemini-3.1-flash-lite',
		'gemini-3-flash-preview',
	);

	/**
	 * Set whether to disable Gemini thinking for faster responses.
	 *
	 * @since 1.0.1
	 *
	 * @param bool $disable True to disable thinking.
	 */
	public function set_disable_thinking( $disable ) {
		$this->disable_thinking = (bool) $disable;
	}

	/**
	 * @inheritDoc
	 */
	public function call( $prompt, $schema = array(), $timeout = 60, $model = '' ) {
		if ( ! $this->has_api_key() ) {
			return new WP_Error( 'missing_key', esc_html__( 'Gemini API key is not configured.', 'masterquiz-ai' ) );
		}

		if ( empty( $model ) ) {
			$models = $this->get_models();
			$model  = ! empty( $models[0] ) ? $models[0] : 'gemini-3.5-flash';
		}

		$result = $this->call_model( $prompt, $schema, $timeout, $model );

		// If Google says "use models/X instead", retry that model once.
		if ( is_wp_error( $result ) ) {
			$suggested = $this->extract_suggested_model( $result->get_error_message() );
			if ( $suggested && $suggested !== $model ) {
				MasterQuiz_Logger::info(
					'Gemini suggested replacement model; retrying.',
					array(
						'from' => $model,
						'to'   => $suggested,
					)
				);
				$result = $this->call_model( $prompt, $schema, $timeout, $suggested );
			}
		}

		return $result;
	}

	/**
	 * Run generateContent for one model ID.
	 *
	 * @param string $prompt  Prompt text.
	 * @param array  $schema  Optional JSON schema.
	 * @param int    $timeout Timeout seconds.
	 * @param string $model   Model ID.
	 * @return string|WP_Error
	 */
	private function call_model( $prompt, $schema, $timeout, $model ) {
		$start_time = microtime( true );
		$is_v3      = $this->is_gemini_3_model( $model );

		$structured_payload = $this->build_payload( $prompt, $schema, $is_v3, true );
		$fallback_payload   = $this->build_payload(
			'Return ONLY JSON matching this schema: ' . wp_json_encode( $schema ) . "\n" . $prompt,
			array(),
			$is_v3,
			false
		);

		$last_error = null;
		foreach ( $this->base_urls as $base_url ) {
			$url = $base_url . rawurlencode( $model ) . ':generateContent?key=' . rawurlencode( $this->api_key );

			$result = $this->make_request( $url, $structured_payload, $timeout );

			if ( is_wp_error( $result ) ) {
				$last_error = $result;
				$code       = (int) $result->get_error_data();

				// Retry without thinking config if that field is rejected.
				if ( 400 === $code && $this->disable_thinking ) {
					$plain = $this->build_payload( $prompt, $schema, $is_v3, true, false );
					$retry = $this->make_request( $url, $plain, $timeout );
					if ( ! is_wp_error( $retry ) ) {
						$result = $retry;
					} else {
						$last_error = $retry;
					}
				}

				// If schema not supported, try unstructured fallback.
				if ( is_wp_error( $result ) && 400 === $code && ! empty( $schema ) ) {
					$result = $this->make_request( $url, $fallback_payload, $timeout );
					if ( is_wp_error( $result ) ) {
						$last_error = $result;
					}
				}
			}

			if ( ! is_wp_error( $result ) ) {
				$elapsed = round( microtime( true ) - $start_time, 2 );
				MasterQuiz_Logger::info( 'Gemini call completed.', array( 'model' => $model, 'seconds' => $elapsed ) );
				return $result;
			}
		}

		$elapsed   = round( microtime( true ) - $start_time, 2 );
		$error_msg = $last_error ? $last_error->get_error_message() : 'Unknown error';
		MasterQuiz_Logger::warning( 'Gemini call failed.', array( 'model' => $model, 'error' => $error_msg, 'seconds' => $elapsed ) );

		return $last_error ? $last_error : new WP_Error(
			'gemini_failed',
			sprintf(
				/* translators: %s: model name */
				esc_html__( 'Gemini API call failed for model %s.', 'masterquiz-ai' ),
				$model
			)
		);
	}

	/**
	 * Build a generateContent request body.
	 *
	 * @param string $prompt           User prompt.
	 * @param array  $schema           Optional response schema.
	 * @param bool   $is_v3            Gemini 3.x model.
	 * @param bool   $with_schema      Attach responseSchema when schema present.
	 * @param bool   $with_thinking    Include thinkingConfig when disabling thinking.
	 * @return array
	 */
	private function build_payload( $prompt, $schema, $is_v3, $with_schema, $with_thinking = true ) {
		$generation = array();

		if ( ! $is_v3 ) {
			$generation['temperature'] = 0.3;
		}

		if ( $with_thinking && $this->disable_thinking ) {
			$generation['thinkingConfig'] = $is_v3
				? array( 'thinkingLevel' => 'minimal' )
				: array( 'thinkingBudget' => 0 );
		}

		if ( $with_schema && ! empty( $schema ) ) {
			$generation['responseMimeType'] = 'application/json';
			$generation['responseSchema']   = $schema;
		}

		$payload = array(
			'contents' => array(
				array(
					'parts' => array(
						array( 'text' => $prompt ),
					),
				),
			),
		);

		if ( ! empty( $generation ) ) {
			$payload['generationConfig'] = $generation;
		}

		return $payload;
	}

	/**
	 * Execute a single HTTP request to the Gemini API.
	 *
	 * @param string $url     Full Gemini endpoint URL.
	 * @param array  $payload Request body.
	 * @param int    $timeout Timeout in seconds.
	 * @return string|WP_Error Response text or error (with HTTP code as error data).
	 */
	private function make_request( $url, $payload, $timeout ) {
		$response = wp_remote_post(
			$url,
			array(
				'timeout' => $timeout,
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode( $payload ),
			)
		);

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'request_failed', $response->get_error_message(), 0 );
		}

		$code = wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( $code < 200 || $code >= 300 ) {
			$msg = isset( $body['error']['message'] ) ? $body['error']['message'] : 'HTTP ' . $code;
			return new WP_Error( 'api_error', $msg, $code );
		}

		// Prefer text parts; skip thought-only parts if present.
		if ( ! empty( $body['candidates'][0]['content']['parts'] ) && is_array( $body['candidates'][0]['content']['parts'] ) ) {
			foreach ( $body['candidates'][0]['content']['parts'] as $part ) {
				if ( ! empty( $part['text'] ) && empty( $part['thought'] ) ) {
					return $part['text'];
				}
			}
			foreach ( $body['candidates'][0]['content']['parts'] as $part ) {
				if ( ! empty( $part['text'] ) ) {
					return $part['text'];
				}
			}
		}

		return new WP_Error( 'invalid_response', esc_html__( 'Invalid Gemini response structure.', 'masterquiz-ai' ), $code );
	}

	/**
	 * @inheritDoc
	 */
	public function get_provider_name() {
		return 'gemini';
	}

	/**
	 * @inheritDoc
	 */
	public function get_models() {
		if ( null !== $this->resolved_models ) {
			return $this->resolved_models;
		}

		$discovered = $this->discover_models();
		if ( ! empty( $discovered ) ) {
			$this->resolved_models = $discovered;
			return $this->resolved_models;
		}

		$this->resolved_models = self::$preferred_models;
		return $this->resolved_models;
	}

	/**
	 * Ask Gemini which models this API key can use, then rank them.
	 *
	 * @return string[]
	 */
	private function discover_models() {
		if ( ! $this->has_api_key() ) {
			return array();
		}

		$url = 'https://generativelanguage.googleapis.com/v1beta/models?pageSize=100&key=' . rawurlencode( $this->api_key );

		$response = wp_remote_get(
			$url,
			array(
				'timeout' => 20,
				'headers' => array( 'Accept' => 'application/json' ),
			)
		);

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

		$code = wp_remote_retrieve_response_code( $response );
		$body = json_decode( wp_remote_retrieve_body( $response ), true );

		if ( $code < 200 || $code >= 300 || empty( $body['models'] ) || ! is_array( $body['models'] ) ) {
			return array();
		}

		$ids = array();
		foreach ( $body['models'] as $model ) {
			if ( empty( $model['name'] ) || ! is_string( $model['name'] ) ) {
				continue;
			}

			$methods = isset( $model['supportedGenerationMethods'] ) ? (array) $model['supportedGenerationMethods'] : array();
			if ( ! in_array( 'generateContent', $methods, true ) ) {
				continue;
			}

			$id = preg_replace( '#^models/#', '', $model['name'] );
			if ( ! is_string( $id ) || 0 !== strpos( $id, 'gemini-' ) ) {
				continue;
			}

			// Skip clearly retired families for new projects.
			if ( preg_match( '/^gemini-(1\.5|2\.0)(-|$)/', $id ) ) {
				continue;
			}

			$ids[] = $id;
		}

		if ( empty( $ids ) ) {
			return array();
		}

		$ids = array_values( array_unique( $ids ) );
		usort( $ids, array( $this, 'compare_model_priority' ) );

		// Keep a tight list for the chain.
		return array_slice( $ids, 0, 6 );
	}

	/**
	 * Sort models: preferred 3.5 flash first, then other 3.x, then anything else.
	 *
	 * @param string $a Model A.
	 * @param string $b Model B.
	 * @return int
	 */
	private function compare_model_priority( $a, $b ) {
		$score_a = $this->model_priority_score( $a );
		$score_b = $this->model_priority_score( $b );
		if ( $score_a === $score_b ) {
			return strcmp( $a, $b );
		}
		return ( $score_a < $score_b ) ? -1 : 1;
	}

	/**
	 * Lower score = higher priority.
	 *
	 * @param string $model Model ID.
	 * @return int
	 */
	private function model_priority_score( $model ) {
		$order = array_flip( self::$preferred_models );
		if ( isset( $order[ $model ] ) ) {
			return $order[ $model ];
		}
		if ( false !== strpos( $model, '3.5-flash' ) && false === strpos( $model, 'lite' ) ) {
			return 10;
		}
		if ( false !== strpos( $model, '3.5-flash-lite' ) ) {
			return 11;
		}
		if ( false !== strpos( $model, '3.1-flash-lite' ) ) {
			return 12;
		}
		if ( 0 === strpos( $model, 'gemini-3' ) ) {
			return 20;
		}
		if ( 0 === strpos( $model, 'gemini-2.5' ) ) {
			return 50;
		}
		return 100;
	}

	/**
	 * Whether the model is a Gemini 3.x family ID.
	 *
	 * @param string $model Model code.
	 * @return bool
	 */
	private function is_gemini_3_model( $model ) {
		return (bool) preg_match( '/^gemini-3(\.|-)/', (string) $model );
	}

	/**
	 * Parse "Please update your code to use `models/NAME`" from Google errors.
	 *
	 * @param string $message API error message.
	 * @return string Empty if none.
	 */
	private function extract_suggested_model( $message ) {
		if ( ! is_string( $message ) || '' === $message ) {
			return '';
		}

		if ( preg_match( '/use `models\/([a-z0-9._-]+)`/i', $message, $m ) ) {
			return $m[1];
		}
		if ( preg_match( '/models\/([a-z0-9._-]+)/i', $message, $m ) && false !== stripos( $message, 'update your code' ) ) {
			return $m[1];
		}

		return '';
	}

	/**
	 * Whether an error means this model ID is dead for this key.
	 *
	 * @param WP_Error $error Error object.
	 * @return bool
	 */
	private function is_unavailable_model_error( $error ) {
		if ( ! is_wp_error( $error ) ) {
			return false;
		}
		$msg = $error->get_error_message();
		return (bool) preg_match( '/no longer available|not found|is not found|not supported for/i', $msg );
	}

	/**
	 * @inheritDoc
	 */
	public function test_connection() {
		if ( ! $this->has_api_key() ) {
			return new WP_Error( 'missing_key', esc_html__( 'API key not provided.', 'masterquiz-ai' ) );
		}

		// Force a fresh discovery for tests.
		$this->resolved_models = null;
		$models                = $this->get_models();
		$last_error            = null;
		$tried                 = array();

		foreach ( $models as $model ) {
			$tried[ $model ] = true;
			$result          = $this->call_model( 'Respond with the JSON: {"status":"ok"}', array(), 25, $model );

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

			$last_error = $result;
			$suggested  = $this->extract_suggested_model( $result->get_error_message() );

			if ( $suggested && empty( $tried[ $suggested ] ) ) {
				$tried[ $suggested ] = true;
				$retry               = $this->call_model( 'Respond with the JSON: {"status":"ok"}', array(), 25, $suggested );
				if ( ! is_wp_error( $retry ) ) {
					return true;
				}
				$last_error = $retry;
			}

			// Keep trying other models when this ID is retired.
			if ( ! $this->is_unavailable_model_error( $result ) ) {
				// Still continue — another model may work.
				continue;
			}
		}

		return $last_error instanceof WP_Error
			? $last_error
			: new WP_Error( 'test_failed', esc_html__( 'Gemini connection test failed.', 'masterquiz-ai' ) );
	}
}
