<?php
/**
 * Gemini AI API class.
 *
 * @package Page_Generator_Pro
 * @author WP Zinc
 */

/**
 * API class for generating content from Google's Gemini AI.
 *
 * @package Page_Generator_Pro
 * @author  WP Zinc
 * @version 4.6.0
 */
class Page_Generator_Pro_Gemini_AI extends Page_Generator_Pro_API {

	use Page_Generator_Pro_AI_Trait;
	use Page_Generator_Pro_Integration_Trait;
	use Page_Generator_Pro_Spintax_Trait;
	use Page_Generator_Pro_Shortcode_Trait;

	/**
	 * Holds the base object.
	 *
	 * @since   4.6.0
	 *
	 * @var     object
	 */
	public $base;

	/**
	 * Holds the programmatic name of this integration.
	 *
	 * @since   4.8.0
	 *
	 * @var     string
	 */
	public $name = 'gemini-ai';

	/**
	 * Holds the API endpoint
	 *
	 * @since   4.6.0
	 *
	 * @var     string
	 */
	public $api_endpoint = 'https://generativelanguage.googleapis.com/v1';

	/**
	 * Holds the flag determining if the request data should be encoded
	 * into a JSON string
	 *
	 * If false, data is encoded using http_build_query()
	 *
	 * @since   4.6.0
	 *
	 * @var     bool
	 */
	public $is_json_request = true;

	/**
	 * Holds the account URL where users can obtain their API key
	 *
	 * @since   4.6.0
	 *
	 * @var     string
	 */
	public $account_url = 'https://aistudio.google.com/app/apikey';

	/**
	 * Holds the referal URL to use for users wanting to sign up
	 * to the API service.
	 *
	 * @since   4.6.0
	 *
	 * @var     string
	 */
	public $referral_url = 'https://aistudio.google.com/app/apikey';

	/**
	 * Holds the URL to the model documentation
	 *
	 * @since   5.1.2
	 *
	 * @var     string
	 */
	public $model_documentation_url = 'https://ai.google.dev/gemini-api/docs/models';

	/**
	 * Constructor.
	 *
	 * @since   4.6.0
	 *
	 * @param   object $base    Base Plugin Class.
	 */
	public function __construct( $base ) {

		// Store base class.
		$this->base = $base;

		// Register Settings Fields.
		add_filter( 'page_generator_pro_integrations_get', array( $this, 'register_integration' ) );
		add_filter( 'page_generator_pro_integrations_get_settings_fields', array( $this, 'ai_settings_fields' ) );

		// Register as a Keyword Source.
		add_filter( 'page_generator_pro_keywords_register_sources', array( $this, 'ai_register_keyword_source' ) );
		add_filter( 'page_generator_pro_keywords_save_' . $this->name, array( $this, 'ai_save_keyword' ) );

		// Register as a Generate Locations Provider.
		add_filter( 'page_generator_pro_keywords_generate_locations_get_providers', array( $this, 'register_integration' ) );
		add_filter( 'page_generator_pro_keywords_generate_locations_get_providers_settings_fields', array( $this, 'ai_settings_fields' ) );
		add_filter( 'page_generator_pro_common_get_locations_output_types_cities_' . $this->name, array( $this, 'ai_generate_locations_output_types_cities' ) );
		add_filter( 'page_generator_pro_common_get_locations_output_types_' . $this->name, array( $this, 'ai_generate_locations_output_types_countries' ) );
		add_filter( 'page_generator_pro_keywords_generate_locations_by_area_' . $this->name, array( $this, 'ai_generate_locations' ), 10, 4 );
		add_filter( 'page_generator_pro_keywords_generate_locations_by_radius_' . $this->name, array( $this, 'ai_generate_locations' ), 10, 4 );

		// Register as a Dynamic Element.
		add_filter( 'page_generator_pro_shortcode_add_shortcodes', array( $this, 'add_shortcode' ) );

		// Register as an AI Dynamic Element Provider.
		add_filter( 'page_generator_pro_shortcode_ai_get_providers', array( $this, 'register_integration' ) );

		// Register as a Research Provider.
		add_filter( 'page_generator_pro_research_get_providers', array( $this, 'register_integration' ) );
		add_filter( 'page_generator_pro_research_get_providers_settings_fields', array( $this, 'ai_settings_fields' ) );
		add_filter( 'page_generator_pro_research_research_' . $this->name, array( $this, 'ai_research' ), 10, 5 );

		// Register as a Spintax Provider.
		add_filter( 'page_generator_pro_spintax_get_providers', array( $this, 'register_spintax_integration' ) );
		add_filter( 'page_generator_pro_spintax_get_providers_settings_fields', array( $this, 'ai_settings_fields' ) );
		add_filter( 'page_generator_pro_spintax_add_spintax_' . $this->name, array( $this, 'ai_add_spintax' ), 10, 2 );

	}

	/**
	 * Returns this shortcode / block's title.
	 *
	 * @since   4.6.0
	 *
	 * @return  string
	 */
	public function get_title() {

		return __( 'Gemini AI', 'page-generator-pro' );

	}

	/**
	 * Returns this shortcode / block's description.
	 *
	 * @since   4.6.0
	 *
	 * @return string
	 */
	public function get_description() {

		return __( 'Displays content from Gemini AI based on a topic.', 'page-generator-pro' );

	}

	/**
	 * Returns this shortcode / block's Keywords, excluding the title.
	 *
	 * @since   4.6.0
	 *
	 * @return  array
	 */
	public function get_keywords() {

		return array(
			__( 'Research', 'page-generator-pro' ),
			__( 'AI', 'page-generator-pro' ),
			__( 'Gemini AI', 'page-generator-pro' ),
		);

	}

	/**
	 * Returns this shortcode / block's icon.
	 *
	 * @since   4.6.0
	 *
	 * @return  string
	 */
	public function get_icon() {

		return 'assets/images/icons/gemini-ai.svg';

	}

	/**
	 * Returns an array of supported models for Gemini AI.
	 *
	 * @since   4.6.0
	 *
	 * @return  array
	 */
	public function get_models() {

		return array(
			// 2.5 Flash Preview.
			'gemini-2.5-flash-preview-04-17' => __( 'Gemini 2.5 Flash (Preview)', 'page-generator-pro' ),

			// 2.5 Pro.
			'gemini-2.5-pro-preview-05-06'   => __( 'Gemini 2.5 Pro (Preview)', 'page-generator-pro' ),
			'gemini-2.5-pro-exp-03-25'       => __( 'Gemini 2.5 Pro (Experimental)', 'page-generator-pro' ),

			// 2.0 Flash.
			'gemini-2.0-flash'               => __( 'Gemini 2.0 Flash (Latest)', 'page-generator-pro' ),
			'gemini-2.0-flash-001'           => __( 'Gemini 2.0 Flash (Stable, v1)', 'page-generator-pro' ),
			'gemini-2.0-flash-exp'           => __( 'Gemini 2.0 Flash (Experimental)', 'page-generator-pro' ),

			// 2.0 Flash Lite.
			'gemini-2.0-flash-lite'          => __( 'Gemini 2.0 Flash Lite (Latest)', 'page-generator-pro' ),
			'gemini-2.0-flash-lite-001'      => __( 'Gemini 2.0 Flash Lite (Stable, v1)', 'page-generator-pro' ),

			// 1.5 Flash.
			'gemini-1.5-flash'               => __( 'Gemini 1.5 Flash (Latest, stable)', 'page-generator-pro' ),
			'gemini-1.5-flash-latest'        => __( 'Gemini 1.5 Flash (Latest)', 'page-generator-pro' ),
			'gemini-1.5-flash-001'           => __( 'Gemini 1.5 Flash (Stable, v1)', 'page-generator-pro' ),
			'gemini-1.5-flash-002'           => __( 'Gemini 1.5 Flash (Stable, v2)', 'page-generator-pro' ),

			// 1.5 Flash-8B.
			'gemini-1.5-flash-8b'            => __( 'Gemini 1.5 Flash-8B (Latest, stable)', 'page-generator-pro' ),
			'gemini-1.5-flash-8b-latest'     => __( 'Gemini 1.5 Flash-8B (Latest)', 'page-generator-pro' ),
			'gemini-1.5-flash-8b-001'        => __( 'Gemini 1.5 Flash-8B (Stable, v1)', 'page-generator-pro' ),

			// 1.5 Pro.
			'gemini-1.5-pro'                 => __( 'Gemini 1.5 Pro (Latest, stable)', 'page-generator-pro' ),
			'gemini-1.5-pro-latest'          => __( 'Gemini 1.5 Pro (Latest)', 'page-generator-pro' ),
			'gemini-1.5-pro-001'             => __( 'Gemini 1.5 Pro (Stable, v1)', 'page-generator-pro' ),
			'gemini-1.5-pro-002'             => __( 'Gemini 1.5 Pro (Stable, v2)', 'page-generator-pro' ),

			// 1.0 Pro.
			'gemini-1.0-pro'                 => __( 'Gemini 1.0 Pro (Latest, stable)', 'page-generator-pro' ),
			'gemini-1.0-pro-latest'          => __( 'Gemini 1.0 Pro (Latest)', 'page-generator-pro' ),
			'gemini-1.0-pro-001'             => __( 'Gemini 1.0 Pro (Stable)', 'page-generator-pro' ),
		);

	}

	/**
	 * Submits a new completion request for the given prompt.
	 *
	 * @since   4.6.0
	 *
	 * @param   string $prompt              Prompt.
	 * @param   string $content_type        Content Type (article).
	 * @param   int    $limit               Limit.
	 * @param   string $language            Language code.
	 * @param   bool   $spintax             Return in Spintax format.
	 * @param   string $model               Model to use.
	 * @param   float  $temperature         Temperature.
	 * @param   float  $top_p               Top P / Nucleus Sampling.
	 * @param   float  $presence_penalty    Presence Penalty.
	 * @param   float  $frequency_penalty   Frequency Penalty.
	 * @return  WP_Error|string
	 */
	public function ai_create_completion( $prompt, $content_type = 'article', $limit = 250, $language = 'en', $spintax = false, $model = 'gemini-2.5-flash-preview-04-17', $temperature = 1, $top_p = 1, $presence_penalty = 0, $frequency_penalty = 0 ) {

		// Fetch prompt.
		$prompt_text = $this->get_prompt(
			array(
				'license_key'  => $this->base->licensing->get_license_key(),
				'prompt'       => $prompt,
				'content_type' => $content_type,
				'limit'        => $limit,
				'language'     => $language,
				'spintax'      => $spintax,
				'model'        => $model,
			)
		);

		// If an error occured, return it now.
		if ( is_wp_error( $prompt_text ) ) {
			return $prompt_text;
		}

		// Calculate maximum tokens, depending on the model used.
		// One token = ~ 4 characters.
		switch ( $model ) {
			case 'gemini-2.5-pro-preview-05-06':
			case 'gemini-2.5-pro-exp-03-25':
			case 'gemini-2.5-flash-preview-04-17':
				// 65,536 token limit.
				$tokens = ( 65536 - strlen( $prompt_text ) );
				break;

			case 'gemini-2.0-flash':
			case 'gemini-2.0-flash-001':
			case 'gemini-2.0-flash-exp':
			case 'gemini-2.0-flash-lite':
			case 'gemini-2.0-flash-lite-001':
			case 'gemini-1.5-flash':
			case 'gemini-1.5-flash-latest':
			case 'gemini-1.5-flash-001':
			case 'gemini-1.5-flash-002':
			case 'gemini-1.5-flash-8b':
			case 'gemini-1.5-flash-8b-latest':
			case 'gemini-1.5-flash-8b-001':
			case 'gemini-1.5-pro':
			case 'gemini-1.5-pro-latest':
			case 'gemini-1.5-pro-001':
			case 'gemini-1.5-pro-002':
				// 8,192 token limit.
				$tokens = ( 8192 - strlen( $prompt_text ) );
				break;

			default:
				// 2,048 token limit.
				$tokens = ( 2048 - strlen( $prompt_text ) );
				break;
		}

		// If the remaining number of tokens is negative, return an error.
		if ( $tokens < 0 ) {
			return new WP_Error(
				'page_generator_pro_ai_error',
				sprintf(
					/* translators: Number of tokens remaining after prompt */
					__( 'The prompt is too long for the selected model. Please shorten the prompt and try again. Maximum tokens: %s', 'page-generator-pro' ),
					$tokens
				)
			);
		}

		// Send request to Gemini AI.
		return $this->query(
			$prompt_text,
			$model,
			array(
				'generationConfig' => array(
					'maxOutputTokens' => $tokens,
					'temperature'     => $temperature,
					'topP'            => $top_p,
				),
			)
		);

	}

	/**
	 * Sends a prompt to Gemini AI, with options to define the model and additional parameters.
	 *
	 * @since   4.6.0
	 *
	 * @param   string $prompt_text    Prompt Text.
	 * @param   string $model          Model.
	 * @param   array  $params         Additional request / query parameters e.g. temperature, top_p.
	 */
	private function query( $prompt_text, $model = 'gemini-1.0-pro', $params = array() ) {

		// Set Headers.
		$this->set_headers(
			array(
				'Content-Type' => 'application/json',
			)
		);

		switch ( $model ) {
			// These models use the v1beta endpoint.
			case 'gemini-2.5-pro-preview-05-06':
			case 'gemini-2.5-pro-exp-03-25':
			case 'gemini-2.5-flash-preview-04-17':
			case 'gemini-2.0-flash-exp':
			case 'gemini-1.5-flash-latest':
			case 'gemini-1.5-flash-8b-latest':
			case 'gemini-1.5-pro-latest':
			case 'gemini-1.0-pro':
			case 'gemini-1.0-pro-latest':
			case 'gemini-1.0-pro-001':
				// Only append beta to the API endpoint if this wasn't done
				// in a previous call in this request lifecycle.
				if ( strpos( $this->api_endpoint, 'beta' ) === false ) {
					$this->api_endpoint .= 'beta';
				}
				break;

			default:
				// Remove beta from the API endpoint, in case it was appended in a previous request
				// in this request lifecycle.
				$this->api_endpoint = str_replace( 'beta', '', $this->api_endpoint );
				break;
		}

		// Make the request to the applicable endpoint.
		$data = $this->response(
			$this->post(
				'models/' . $model . ':generateContent?key=' . $this->ai_get_api_key(),
				array_merge(
					array(
						'contents' => array(
							'role'  => 'user',
							'parts' => array(
								'text' => $prompt_text,
							),
						),
					),
					$params
				)
			)
		);

		// Bail if an error occured.
		if ( is_wp_error( $data ) ) {
			return $data;
		}

		// Fetch and return the text response.
		return trim( $data->candidates[0]->content->parts[0]->text );

	}

}
