<?php
/**
 * Provider_Type Base Class.
 *
 * Abstract base class for all LLM provider type implementations. Defines
 * common methods for model creation and parameter management.
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://agenistrator.com/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Provider_Type class.
 */
abstract class Provider_Type {
	/**
	 * Provider ID - unique identifier for this provider
	 * @var        string
	 * @since     1.0.0
	 */
	protected $id = '';

	/**
	 * Provider display name - user-friendly name for this instance
	 * @var        string
	 * @since     1.0.0
	 */
	protected $display_name = '';

	/**
	 * Provider parameters configuration
	 * @var        array
	 * @since     1.0.0
	 */
	protected $parameters = array();

	/**
	 * Get the provider type ID
	 *
	 * Convention: lowercase kebab-case (e.g., 'openai', 'anthropic', 'ollama-cloud').
	 *  - MUST be lowercase. WordPress sanitize_key() (used by AJAX handlers) lowercases
	 *    input, so lookups fail if the type ID contains uppercase characters.
	 *  - SHOULD use hyphens as the separator for multi-word ids. Hyphens are valid
	 *    input for sanitize_key(), which permits [a-z0-9_-].
	 *
	 * @return     string Lowercase kebab-case provider type identifier
	 * @since     1.0.0
	 */
	abstract public function get_type_id();

	/**
	 * Get the human-readable display name for this provider type
	 *
	 * Used in UI surfaces such as the provider type dropdown on the Add New
	 * Provider form. Distinct from get_display_name(), which returns the
	 * per-instance friendly name chosen by the user.
	 *
	 * @return     string Localized provider type display name (e.g., 'OpenAI', 'Ollama Cloud')
	 * @since     1.0.1
	 */
	abstract public function get_type_display_name();

	/**
	 * Get the models.dev API key for this provider
	 *
	 * Used for looking up model info from the models.dev API. The default
	 * implementation returns the lowercased type ID. Override when the
	 * models.dev catalog uses a different key for this provider.
	 *
	 * @return     string The models.dev provider key
	 * @since     1.0.0
	 */
	public function get_models_dev_key() {
		return strtolower( $this->get_type_id() );
	}

	/**
	 * Get the provider ID
	 * @return     string
	 * @since     1.0.0
	 */
	public function get_id() {
		return $this->id;
	}

	/**
	 * Get the provider display name
	 * @return     string
	 * @since     1.0.0
	 */
	public function get_display_name() {
		return $this->display_name;
	}

	/**
	 * Set the provider display name
	 * @param      string $display_name The display name
	 * @return     Provider_Type This instance for method chaining
	 * @since     1.0.0
	 */
	public function set_display_name( $display_name ) {
		$this->display_name = $display_name;
		return $this;
	}

	/**
	 * Constructor
	 *
	 * @param      string $id The provider ID
	 * @param      string $display_name The display name
	 * @param      array $parameters The parameters
	 * @since     1.0.0
	 */
	public function __construct( $id = '', $display_name = '', $parameters = array() ) {
		if ( ! empty( $id ) ) {
			$this->id = $id;
		}

		if ( ! empty( $display_name ) ) {
			$this->display_name = $display_name;
		}

		if ( ! empty( $parameters ) ) {
			$this->parameters = $parameters;
		}
	}

	/**
	 * Get all parameters for this provider
	 * @return     array Parameters as key-value pairs
	 * @since     1.0.0
	 */
	public function get_params() {
		return $this->parameters;
	}

	/**
	 * Set all parameters for this provider
	 * @param      array $params Parameters as key-value pairs
	 * @return     Provider_Type This instance for method chaining
	 * @since     1.0.0
	 */
	public function set_params( $params ) {
		$this->parameters = $params;
		return $this;
	}

	/**
	 * Get a specific parameter value
	 * @param      string $param_name The parameter name
	 * @param      mixed $default_value Default value if parameter doesn't exist
	 * @return     mixed Parameter value or default
	 * @since     1.0.0
	 */
	public function get_param( $param_name, $default_value = '' ) {
		return $this->parameters[ $param_name ] ?? $default_value;
	}

	/**
	 * Set a specific parameter value
	 * @param      string $param_name The parameter name
	 * @param      mixed $value The parameter value
	 * @return     Provider_Type This instance for method chaining
	 * @since     1.0.0
	 */
	public function set_param( $param_name, $value ) {
		$this->parameters[ $param_name ] = $value;
		return $this;
	}

	/**
	 * Get the max tokens per minute rate limit for this provider
	 *
	 * Returns the provider's configured TPM limit, or null if the provider
	 * has no rate limiting (e.g., OpenRouter).
	 *
	 * @return     int|null TPM limit or null for no limit
	 * @since     1.0.0
	 */
	public function get_max_tokens_per_minute() {
		return null;
	}

	/**
	 * Check if the provider is configured
	 *
	 * This method determines if the provider has the necessary configuration
	 * to be used. Child classes should override this to check their specific
	 * required parameters.
	 *
	 * @return     bool True if configured, false otherwise
	 * @since     1.0.0
	 */
	public function is_configured() {
		// Default implementation checks if required parameters are set
		// Each provider should define its required parameters
		foreach ( $this->get_required_parameters() as $param ) {
			if ( empty( $this->get_param( $param ) ) ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Get the required parameters for this provider
	 *
	 * @return     array Array of required parameter names
	 * @since     1.0.0
	 */
	public function get_required_parameters() {
		// Default implementation assumes API key is required
		// Child classes should override this to specify their required parameters
		return array( 'api_key' );
	}

	/**
	 * Get the parameter definitions for this provider
	 *
	 * Each parameter definition must include:
	 * - 'name' (string): Display name for the parameter
	 * - 'type' (string): Input type ('text', 'password', 'number')
	 * - 'description' (string): Help text describing the parameter (required)
	 * - 'required' (bool): Whether the parameter is required
	 *
	 * Optional fields:
	 * - 'default' (mixed): Default value
	 * - 'min' (number): Minimum value for number inputs
	 * - 'max' (number): Maximum value for number inputs
	 * - 'step' (number): Step value for number inputs
	 * - 'private' (bool): Whether the parameter should be treated as sensitive
	 *
	 * @return     array Array of parameter definitions keyed by parameter name
	 * @since     1.0.0
	 */
	abstract public function get_parameter_definitions();

	/**
	 * Get the URL to obtain an API key for this provider
	 *
	 * Returns the URL to the provider's API key management page where
	 * users can create or obtain their API keys.
	 *
	 * @return     string URL to the provider's API key page
	 * @since     1.0.0
	 */
	abstract public function get_api_key_url();

	/**
	 * Test if the provider configuration is valid by making an API call
	 *
	 * This method tests if the provider's configuration is valid by attempting
	 * to make a simple API call.
	 *
	 * @throws     \Exception If configuration is invalid or API call fails
	 * @since     1.0.0
	 */
	abstract public function test_configuration();

	/**
	 * Create a Chat model instance
	 *
	 * @param      string $model_id The model ID
	 * @return     LLM_Chat Chat model instance
	 * @since     1.0.0
	 */
	abstract public function create_chat_model( string $model_id );

	/**
	 * Create an Embedding model instance
	 *
	 * @param      string $model_id The model ID
	 * @return     LLM_Embedding Embedding model instance
	 * @since     1.0.0
	 */
	abstract public function create_embedding_model( string $model_id );

	/**
	 * Get all available models from the provider API
	 *
	 * This method retrieves all available models from the provider API.
	 * Child classes should implement this method based on their specific API.
	 *
	 * Note: Use get_available_models_cached() for performance reasons.
	 *
	 * @return     array Models categorized by type (chat and embedding)
	 * @since     1.0.0
	 */
	abstract public function get_available_models();

	/**
	 * Get all available models from the provider API with caching
	 *
	 * This method wraps get_available_models() with WordPress transient-based caching
	 * to avoid repeated API calls. The cache is stored per provider instance and
	 * expires after one day.
	 *
	 * @param      bool   $flush_cache  Whether force fresh API call and update cache
	 * @return     array  Models categorized by type (chat and embedding)
	 * @since     1.0.0
	 */
	public function get_available_models_cached( $flush_cache = false ) {
		// Generate unique transient key for this provider instance
		$transient_key = $this->get_models_cache_transient_key();

		if ( ! $flush_cache ) {
			// Check if we have a cached version
			$cached_json = get_transient( $transient_key );
			if ( is_string( $cached_json ) ) {
				$cached_data = json_decode( $cached_json, true );
				if ( is_array( $cached_data ) ) {
					// Verify the cached data is for this provider (guard against md5 collisions)
					if ( isset( $cached_data['provider_id'] ) && $cached_data['provider_id'] === $this->id ) {
						return $cached_data['models'];
					}
				}
				// Cache miss or collision detected - fall through to fetch fresh data
			}
		}

		// Cache miss, collision, or flush requested - fetch from API
		$models = $this->get_available_models();

		// Store in transient with provider ID for collision detection
		$cache_data = array(
			'provider_id' => $this->id,
			'models'      => $models,
		);
		set_transient( $transient_key, wp_json_encode( $cache_data ), DAY_IN_SECONDS );

		return $models;
	}

	/**
	 * Clear the cached models for this provider instance
	 *
	 * Call this method when provider configuration changes to ensure
	 * fresh model data is fetched on next access.
	 *
	 * @return     bool  True if cache was deleted, false otherwise
	 * @since     1.0.0
	 */
	public function clear_models_cache() {
		$transient_key = $this->get_models_cache_transient_key();
		return delete_transient( $transient_key );
	}

	/**
	 * Get the transient cache key for this provider instance
	 *
	 * @return     string  The transient key
	 */
	private function get_models_cache_transient_key() {
		return 'agenistrator_models_' . md5( $this->id ); // md5 is a quick way to comply with transient key requirements
	}

	/**
	 * Get the default chat model ID
	 *
	 * Returns the provider's preferred default chat model ID.
	 * Returns null if the provider doesn't have a default or if
	 * the default model is not available at the provider.
	 *
	 * @return     string|null Default chat model ID or null
	 * @since     1.0.0
	 */
	abstract public function get_default_chat_model();

	/**
	 * Get the default embedding model ID
	 *
	 * Returns the provider's preferred default embedding model ID.
	 * Returns null if the provider doesn't support embeddings or if
	 * the default model is not available at the provider.
	 *
	 * @return     string|null Default embedding model ID or null
	 * @since     1.0.0
	 */
	abstract public function get_default_embedding_model();

	/**
	 * Enable this provider
	 * @return     bool Success or failure
	 * @since     1.0.0
	 */
	public function enable() {
		return Provider_Manager::get_instance()->enable_provider( $this->id );
	}

	/**
	 * Disable this provider
	 * @return     bool Success or failure
	 * @since     1.0.0
	 */
	public function disable() {
		return Provider_Manager::get_instance()->disable_provider( $this->id );
	}

	/**
	 * Check if this provider is enabled
	 * @return     bool True if enabled, false otherwise
	 * @since     1.0.0
	 */
	public function is_enabled() {
		return Provider_Manager::get_instance()->is_provider_enabled( $this->id );
	}

	/**
	 * Get model limits (context window and max output tokens) for a specific model
	 *
	 * Looks up the model in cached available models. Only returns values if
	 * the provider supplies authoritative data (e.g., OpenRouter API).
	 * Returns nulls for providers that don't supply this data (OpenAI, Anthropic, Ollama Cloud).
	 *
	 * @param      string $model_id The model ID to look up
	 * @return     array ['context_length' => int|null, 'max_output_tokens' => int|null]
	 * @since     1.0.0
	 */
	public function get_model_limits( $model_id ) {
		$null_limits = array(
			'context_length'    => null,
			'max_output_tokens' => null,
		);

		if ( ! $this->is_configured() ) {
			return $null_limits;
		}

		try {
			$models = $this->get_available_models_cached();
			foreach ( $models['chat'] ?? array() as $model ) {
				if ( ( $model['id'] ?? '' ) === $model_id ) {
					return array(
						'context_length'    => $model['context_length'] ?? null,
						'max_output_tokens' => $model['max_output_tokens'] ?? null,
					);
				}
			}
		} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			// Ignore errors, return nulls
		}

		return $null_limits;
	}

	/**
	 * Check if this provider supports a specific model
	 *
	 * @param      string $model_id The model ID to check
	 * @return     bool True if this provider supports the model, false otherwise
	 * @since     1.0.0
	 */
	public function has_model( $model_id ) {
		if ( ! $this->is_configured() ) {
			return false;
		}

		try {
			$available_models = $this->get_available_models_cached();

			// Check in chat models
			if ( isset( $available_models['chat'] ) && is_array( $available_models['chat'] ) ) {
				foreach ( $available_models['chat'] as $model ) {
					if ( isset( $model['id'] ) && $model['id'] === $model_id ) {
						return true;
					}
				}
			}

			// Check in embedding models
			if ( isset( $available_models['embedding'] ) && is_array( $available_models['embedding'] ) ) {
				foreach ( $available_models['embedding'] as $model ) {
					if ( isset( $model['id'] ) && $model['id'] === $model_id ) {
						return true;
					}
				}
			}

			return false;
		} catch ( \Throwable $e ) {
			return false;
		}
	}
}
