<?php
/**
 * OpenRouter Provider.
 *
 * Implements the provider interface for OpenRouter. Uses the orhanerday/open-ai
 * package with OpenRouter's OpenAI-compatible endpoint.
 *
 * @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://maximum.software/
 *
 * 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' ) ) {
	return;
}

/**
 * Provider_OpenRouter class.
 */
class Provider_OpenRouter extends Provider_OpenAI {

	/**
	 * Get the provider type ID
	 *
	 * @return     string Lowercase provider type identifier
	 * @since     1.0.0
	 */
	public function get_type_id() {
		return 'openrouter';
	}

	/**
	 * Get the parameter definitions for this provider
	 *
	 * @return     array Array of parameter definitions
	 * @since     1.0.0
	 */
	public function get_parameter_definitions() {
		return array(
			'api_key' => array(
				'name'        => 'API Key',
				'type'        => 'password',
				'description' => 'Your OpenRouter API key. Get it from your OpenRouter dashboard.',
				'required'    => true,
			),
		);
	}

	/**
	 * Get the URL to obtain an API key for this provider
	 *
	 * @return     string URL to the OpenRouter API keys page
	 * @since     1.0.0
	 */
	public function get_api_key_url() {
		return 'https://openrouter.ai/keys';
	}

	/**
	 * Get the required parameters for this provider
	 *
	 * @return     array Array of required parameter names
	 * @since     1.0.0
	 */
	public function get_required_parameters() {
		return array( 'api_key' );
	}

	/**
	 * Get the max tokens per minute rate limit for this provider
	 *
	 * OpenRouter API doesn't have TPM rate limits, so we disable it.
	 *
	 * @return     int|null TPM limit or null to disable rate limiting
	 * @since     1.0.0
	 */
	public function get_max_tokens_per_minute() {
		return null;
	}

	/**
	 * Override base URL for OpenRouter's OpenAI-compatible endpoint
	 *
	 * Note: The orhanerday/open-ai library replaces Url::ORIGIN (https://api.openai.com)
	 * with this custom URL, and the library already appends /v1 to paths.
	 * So we use https://openrouter.ai/api (without /v1) to get the correct final URL:
	 * https://openrouter.ai/api/v1/models, https://openrouter.ai/api/v1/chat/completions, etc.
	 *
	 * @return     string|null
	 * @since     1.0.0
	 */
	protected function get_client_base_url() {
		return 'https://openrouter.ai/api';
	}

	/**
	 * Override headers to include OpenRouter API key and other info
	 *
	 * @return     array
	 * @since     1.0.0
	 */
	protected function get_client_headers() {
		return array(
			'User-Agent: Agenistrator/' . AGENISTRATOR_VERSION,
			'HTTP-Referer: ' . get_site_url(),
			'X-Title: Agenistrator/' . AGENISTRATOR_VERSION,
		);
	}

	/**
	 * Create a Chat model instance
	 *
	 * @param      string $model_id The model ID
	 * @return     LLM_Chat_OpenRouter Chat model instance
	 * @since     1.0.0
	 */
	public function create_chat_model( string $model_id ) {
		return new LLM_Chat_OpenRouter( $model_id, $this );
	}

	/**
	 * Create an Embedding model instance
	 *
	 * @param      string $model_id The model ID
	 * @return     LLM_Embedding_OpenRouter Embedding model instance
	 * @since     1.0.0
	 */
	public function create_embedding_model( string $model_id ) {
		return new LLM_Embedding_OpenRouter( $model_id, $this );
	}

	/**
	 * Get all available models from the provider API
	 *
	 * OpenRouter uses two separate endpoints:
	 * - GET /api/v1/models for chat models
	 * - GET /api/v1/embeddings/models for embedding models
	 *
	 * @return     array Models categorized by type (chat and embedding)
	 * @since     1.0.0
	 */
	public function get_available_models() {
		if ( ! $this->is_configured() ) {
			throw new \Exception( 'Provider is not properly configured to get available models.' );
		}

		if ( null !== $this->available_models ) {
			return $this->available_models;
		}

		$models = array(
			'chat'      => array(),
			'embedding' => array(),
		);

		// Fetch chat models from /api/v1/models
		$chat_models = $this->fetch_chat_models();
		foreach ( $chat_models as $model ) {
			if ( $this->is_chat_compatible( $model ) ) {
				$model_id         = $model['id'] ?? '';
				$models['chat'][] = array(
					'id'                => $model_id,
					'name'              => $model['name'] ?? $model_id,
					'context_length'    => $model['context_length'] ?? null,
					'max_output_tokens' => $model['top_provider']['max_completion_tokens'] ?? null,
				);
			}
		}

		// Fetch embedding models from /api/v1/embeddings/models
		$embedding_models = $this->fetch_embedding_models();
		foreach ( $embedding_models as $model ) {
			$model_id              = $model['id'] ?? '';
			$models['embedding'][] = array(
				'id'   => $model_id,
				'name' => $model['name'] ?? $model_id,
			);
		}

		$this->available_models = $models;

		return $models;
	}

	/**
	 * Fetch chat models from /api/v1/models endpoint
	 *
	 * @return     array Array of model data from API
	 * @throws     \Exception On API error
	 * @since     1.0.0
	 */
	protected function fetch_chat_models() {
		$client        = $this->get_client();
		$response_json = $client->listModels();

		try {
			$response = String_Utils::decode_json( $response_json );
		} catch ( \Throwable $e ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception(
				String_Utils::replace_tags(
					__( 'Invalid API response: {error}', 'agenistrator' ),
					array( 'error' => $e->getMessage() )
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		if ( isset( $response['error']['message'] ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( $response['error']['message'] );
		}

		return $response['data'] ?? array();
	}

	/**
	 * Fetch embedding models from /api/v1/embeddings/models endpoint
	 *
	 * The orhanerday/open-ai library doesn't have a method for this endpoint,
	 * so we make a direct HTTP request using WordPress functions.
	 *
	 * @return     array Array of embedding model data from API
	 * @since     1.0.0
	 */
	protected function fetch_embedding_models() {
		$response = wp_remote_get(
			'https://openrouter.ai/api/v1/embeddings/models',
			array(
				'timeout'    => 30,
				'user-agent' => 'Agenistrator/' . AGENISTRATOR_VERSION,
				'headers'    => array(
					'Authorization' => 'Bearer ' . $this->get_param( 'api_key' ),
					'HTTP-Referer'  => get_site_url(),
					'X-Title'       => 'Agenistrator/' . AGENISTRATOR_VERSION,
					'Accept'        => 'application/json',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			// Log error but don't throw - embedding models are optional
			return array();
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( ! is_array( $data ) || ! isset( $data['data'] ) ) {
			return array();
		}

		return $data['data'];
	}

	/**
	 * Returns the default chat model ID.
	 *
	 * Uses a free model so new OpenRouter users can use the plugin without adding credits.
	 *
	 * @return     string|null Default chat model ID or null if not available
	 * @since     1.0.0
	 */
	public function get_default_chat_model() {
		$default = 'openai/gpt-oss-120b';
		return $this->has_model( $default ) ? $default : null;
	}

	/**
	 * Returns the default embedding model ID.
	 *
	 * OpenRouter has no free embedding models, so we return null to indicate
	 * that embeddings should be disabled by default for this provider.
	 *
	 * @return     string|null Default embedding model ID or null if none
	 * @since     1.0.0
	 */
	public function get_default_embedding_model() {
		return null;
	}

	/**
	 * Check if a model supports chat with tool calling
	 *
	 * Uses the `architecture` and `supported_parameters` fields from the
	 * OpenRouter API response to determine compatibility.
	 *
	 * @param      array $model The model data from API (not just model ID)
	 * @return     bool True if the model supports text input, text output, and tools
	 * @since     1.0.0
	 */
	protected function is_chat_compatible( $model ) {
		$input_modalities  = $model['architecture']['input_modalities'] ?? array();
		$output_modalities = $model['architecture']['output_modalities'] ?? array();
		$supported_params  = $model['supported_parameters'] ?? array();

		return in_array( 'text', $input_modalities, true )
			&& in_array( 'text', $output_modalities, true )
			&& in_array( 'tools', $supported_params, true );
	}

	/**
	 * Get the models.dev API key for looking up model info
	 *
	 * For OpenRouter, we need to extract the vendor prefix from the model ID
	 * since OpenRouter aggregates models from multiple providers.
	 * This is handled at the LLM_Chat level, so we return a fallback here.
	 *
	 * @return     string The models.dev provider key
	 * @since     1.0.0
	 */
	public function get_models_dev_key() {
		return 'openrouter';
	}

	/**
	 * Test if the provider configuration is valid
	 *
	 * OpenRouter's /api/v1/models endpoint is public and doesn't require authentication,
	 * so we can't use the parent's approach of fetching models to validate the API key.
	 * Instead, we use the /api/v1/key endpoint which requires a valid API key.
	 *
	 * @see https://openrouter.ai/docs/api/reference/limits
	 * @throws     \Exception If configuration is invalid or API call fails
	 * @since     1.0.0
	 */
	public function test_configuration() {
		if ( ! $this->is_configured() ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Provider is not configured', 'agenistrator' ) );
		}

		$response = wp_remote_get(
			'https://openrouter.ai/api/v1/key',
			array(
				'timeout'    => 30,
				'user-agent' => 'Agenistrator/' . AGENISTRATOR_VERSION,
				'headers'    => array(
					'Authorization' => 'Bearer ' . $this->get_param( 'api_key' ),
					'Accept'        => 'application/json',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( $response->get_error_message() );
		}

		$status_code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $status_code ) {
			$body          = wp_remote_retrieve_body( $response );
			$data          = json_decode( $body, true );
			$error_message = $data['error']['message'] ?? $data['error'] ?? __( 'Invalid API key', 'agenistrator' );
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( $error_message );
		}
	}
}
