<?php

namespace Limb_Chatbot\Includes\AI_Providers\Open_Router\Services;

/**
 * Class Model_Filter_Service
 *
 * Responsible for filtering and parsing OpenRouter model data.
 * Provides methods to filter chat models and parse model attributes.
 *
 * Note: Embedding models are fetched from dedicated /embeddings/models endpoint,
 * so no filtering is needed for them.
 *
 * @package Limb_Chatbot\Includes\AI_Providers\Open_Router\Services
 * @since 1.0.14
 */
class Model_Filter_Service {

	/**
	 * Filters models for chat completion use with function calling support.
	 *
	 * Criteria:
	 * - Must have 'text' in input_modalities
	 * - Must have ONLY 'text' in output_modalities
	 * - Must NOT be expired
	 * - Must support function calling (tools)
	 *
	 * @param array $models Array of model objects from /models endpoint.
	 * @return array Filtered array of chat models with tool support.
	 * @since 1.0.14
	 */
	public function filter_chat_models( array $models ): array {
		return array_filter( $models, function ( $model ) {
			$model_id = $this->get_model_name( $model );
			if ( $this->is_excluded_model( $model_id ) ) {
				return false;
			}

			return $this->is_chat_model( $model ) && $this->supports_function_calling( $model );
		} );
	}

	/**
	 * Checks if a model is suitable for chat completions.
	 *
	 * Criteria:
	 * - input_modalities must contain 'text' (can have other modalities too)
	 * - output_modalities must have ONLY 'text' (exactly ['text'])
	 * - Must NOT be expired
	 *
	 * @param object $model Model data from API.
	 * @return bool True if model supports chat.
	 * @since 1.0.14
	 */
	public function is_chat_model( $model ): bool {
		// Check for expiration
		if ( ! empty( $model->expiration_date ) ) {
			return false;
		}

		$input_modalities  = $this->get_input_modalities( $model );
		$output_modalities = $this->get_output_modalities( $model );

		// Input must contain 'text' (can have other modalities like image, audio, video)
		$has_text_input = in_array( 'text', $input_modalities, true );
		$has_text_output = in_array( 'text', $output_modalities, true );

		return $has_text_input && $has_text_output;
	}

	/**
	 * Checks if a model supports function calling (tools).
	 *
	 * @param object $model Model data from API.
	 * @return bool True if model supports function calling.
	 * @since 1.0.14
	 */
	public function supports_function_calling( $model ): bool {
		$supported_params = $model->supported_parameters ?? [];

		return in_array( 'tools', $supported_params, true )
		    || in_array( 'tool_choice', $supported_params, true );
	}

	/**
	 * Gets input modalities from model data.
	 *
	 * @param object $model Model data from API.
	 * @return array Array of input modality strings.
	 * @since 1.0.14
	 */
	public function get_input_modalities( $model ): array {
		return $model->architecture->input_modalities ?? [];
	}

	/**
	 * Gets output modalities from model data.
	 *
	 * @param object $model Model data from API.
	 * @return array Array of output modality strings.
	 * @since 1.0.14
	 */
	public function get_output_modalities( $model ): array {
		return $model->architecture->output_modalities ?? [];
	}

	/**
	 * Parses modalities from model data into standardized format.
	 *
	 * Input modalities from API: 'text', 'image', 'file', 'audio', 'video'
	 * Output modalities from API: 'text', 'image', 'embeddings', 'audio'
	 *
	 * @param object $model Model data from API.
	 * @return array Modalities array with text, image, audio, file, video keys.
	 * @since 1.0.14
	 */
	public function parse_modalities( $model ): array {
		$input_modalities  = $this->get_input_modalities( $model );
		$output_modalities = $this->get_output_modalities( $model );

		return [
			'text'  => in_array( 'text', $input_modalities, true ) && in_array( 'text', $output_modalities, true ),
			'image' => in_array( 'image', $input_modalities, true ) || in_array( 'image', $output_modalities, true ),
			'audio' => in_array( 'audio', $input_modalities, true ) || in_array( 'audio', $output_modalities, true ),
			'file'  => in_array( 'file', $input_modalities, true ),
			'video' => in_array( 'video', $input_modalities, true ),
		];
	}

	/**
	 * Parses endpoints/features from model data.
	 *
	 * Determines available endpoints based on:
	 * - output_modalities: 'text', 'image', 'embeddings', 'audio'
	 * - input_modalities: 'text', 'image', 'file', 'audio', 'video'
	 * - supported_parameters: 'tools', 'reasoning', etc.
	 *
	 * Note: This is used for chat models from /models endpoint.
	 * Embedding models from /embeddings/models have their own endpoint configuration.
	 *
	 * @param object $model Model data from API.
	 * @return array Endpoints array.
	 * @since 1.0.14
	 */
	public function parse_endpoints( $model ): array {
		$input_modalities   = $this->get_input_modalities( $model );
		$output_modalities  = $this->get_output_modalities( $model );
		$supported_params   = $model->supported_parameters ?? [];

		$has_text_input     = in_array( 'text', $input_modalities, true );
		$has_text_output    = in_array( 'text', $output_modalities, true );
		$has_embeddings     = in_array( 'embeddings', $output_modalities, true );

		// Check for tool/function calling support (indicates Responses API compatibility).
		$has_tools_support  = in_array( 'tools', $supported_params, true )
		                   || in_array( 'tool_choice', $supported_params, true );

		return [
			'chat_completions'  => $has_text_input && $has_text_output,
			'realtime'          => false,
			'batch'             => false,
			'embeddings'        => $has_embeddings,
			'speech_generation' => false,
			'translation'       => false,
			'responses'         => $has_text_input && $has_text_output && $has_tools_support,
			'assistants'        => false,
			'fine_tuning'       => false,
			'image_generation'  => false,
			'transcription'     => false,
			'moderation'        => false,
		];
	}

	/**
	 * Parses features from model data.
	 *
	 * Supported parameters from API:
	 * - temperature, top_p, top_k, min_p, top_a
	 * - frequency_penalty, presence_penalty, repetition_penalty
	 * - max_tokens, logit_bias, logprobs, top_logprobs
	 * - seed, response_format, structured_outputs, stop
	 * - tools, tool_choice, parallel_tool_calls
	 * - include_reasoning, reasoning, reasoning_effort
	 * - web_search_options, verbosity
	 *
	 * Note: Free variants (:free) have function_calling disabled because
	 * OpenRouter cannot route to providers that support tool use for free tier.
	 * See: https://openrouter.ai/docs/guides/routing/provider-selection
	 *
	 * @param object $model Model data from API.
	 * @return array Features array.
	 * @since 1.0.14
	 */
	public function parse_features( $model ): array {
		$supported_params = $model->supported_parameters ?? [];
		// Check if model advertises tool support.
		$has_tools_support = in_array( 'tools', $supported_params, true )
		                  || in_array( 'tool_choice', $supported_params, true );

		return [
			'streaming'            => true, // OpenRouter supports streaming for all models.
			'structured_outputs'   => in_array( 'structured_outputs', $supported_params, true ),
			'distillation'         => false,
			'function_calling'     => $has_tools_support,
			'fine_tuning'          => false,
			'predicted_outputs'    => false,
			'response_format'      => in_array( 'response_format', $supported_params, true ),
		];
	}

	/**
	 * Parses pricing from model data.
	 *
	 * OpenRouter returns pricing as string per token.
	 *
	 * @param object $model Model data from API.
	 * @return array Metas array with pricing.
	 * @since 1.0.14
	 */
	public function parse_pricing( $model ): array {
		$pricing = $model->pricing ?? (object) [];

		$input_cost  = isset( $pricing->prompt ) ? (float) $pricing->prompt : 0.0;
		$output_cost = isset( $pricing->completion ) ? (float) $pricing->completion : 0.0;

		return [
			'input_token_cost'  => $input_cost,
			'output_token_cost' => $output_cost,
		];
	}

	/**
	 * Gets context length from model data.
	 *
	 * @param object $model Model data from API.
	 * @return int Context length.
	 * @since 1.0.14
	 */
	public function get_context_length( $model ): int {
		return (int) ( $model->context_length ?? 4096 );
	}

	/**
	 * Gets output token limit from model data.
	 *
	 * @param object $model Model data from API.
	 * @return int Output token limit.
	 * @since 1.0.14
	 */
	public function get_output_token_limit( $model ): int {
		return (int) ( $model->top_provider->max_completion_tokens
		            ?? $model->per_request_limits->completion_tokens
		            ?? 4096 );
	}

	/**
	 * Gets model name/label from model data.
	 *
	 * @param object $model Model data from API.
	 * @return string Model name.
	 * @since 1.0.14
	 */
	public function get_model_name( $model ): string {
		return $model->id ?? '';
	}

	/**
	 * Gets model display label from model data.
	 *
	 * @param object $model Model data from API.
	 * @return string Model label.
	 * @since 1.0.14
	 */
	public function get_model_label( $model ): string {
		return $model->name ?? $this->get_model_name( $model );
	}

	/**
	 * Checks if a model is a free variant.
	 *
	 * @param object $model Model data from API.
	 * @return bool True if model is a :free variant.
	 * @since 1.0.14
	 */
	public function is_free_variant( $model ): bool {
		$model_name = $this->get_model_name( $model );

		return str_ends_with( $model_name, ':free' );
	}

	/**
	 * Gets the variant suffix from a model name.
	 *
	 * @param object $model Model data from API.
	 * @return string|null Variant suffix or null.
	 * @since 1.0.14
	 */
	public function get_variant( $model ): ?string {
		$model_name = $this->get_model_name( $model );
		$variants   = [ ':free', ':extended', ':exacto', ':thinking', ':online', ':nitro' ];

		foreach ( $variants as $variant ) {
			if ( str_ends_with( $model_name, $variant ) ) {
				return $variant;
			}
		}

		return null;
	}

	/**
	 * Builds a short, user-facing description from OpenRouter model metadata (Listeo-style).
	 *
	 * @param object $model Model data from API.
	 * @return string Short label for model select UI.
	 * @since 2.0.1
	 */
	public function build_description( $model ): string {
		if ( $this->is_free_variant( $model ) ) {
			return 'Free';
		}

		$supported_params = $model->supported_parameters ?? [];
		$variant          = $this->get_variant( $model );
		$pricing          = $this->parse_pricing( $model );
		$output_cost      = (float) ( $pricing['output_token_cost'] ?? 0 );
		$model_name       = $this->get_model_name( $model );

		if ( $variant === ':thinking' || in_array( 'reasoning', $supported_params, true ) || in_array( 'include_reasoning', $supported_params, true ) ) {
			return 'High Intelligence & Slow';
		}

		if ( $output_cost > 0 && $output_cost <= 0.000001 ) {
			return 'Fastest & Cheapest';
		}

		if ( $output_cost >= 0.00001 ) {
			if ( preg_match( '/\b(5\.5|5\.4|4\.7|4\.6|4\.3|opus|pro-preview)\b/i', $model_name ) ) {
				return 'High Intelligence - Latest';
			}

			return 'High Intelligence';
		}

		return 'Fast & Good';
	}

	/**
	 * Checks if a model is excluded from the chat model list.
	 *
	 * @param string $model_id Model ID from API.
	 * @return bool True if excluded.
	 * @since 1.0.14
	 */
	private function is_excluded_model( string $model_id ): bool {
		if ( $model_id === '' ) {
			return false;
		}

		foreach ( $this->get_excluded_model_ids() as $excluded_model_id ) {
			if ( $excluded_model_id !== '' && false !== strpos( $model_id, $excluded_model_id ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Gets hardcoded excluded model IDs from docs/OpenRouter/excluded-models.md.
	 *
	 * @return array Excluded model IDs.
	 * @since 1.0.15
	 */
	private function get_excluded_model_ids(): array {
		return [
			'openai/gpt-5.1-codex',
			'openai/gpt-5.2-codex',
			'openai/gpt-5-codex',
			'x-ai/grok-code',
			'arcee-ai/trinity-large-preview',
			'moonshotai/kimi-k2.5',
			'moonshotai/kimi-k2-thinking',
			'minimax/minimax-m2.1',
			'z-ai/glm-4.7',
			'google/gemini-3-flash-preview',
			'openai/gpt-5.2',
			'qwen/qwen3-coder',
			'xiaomi/mimo-v2-flash',
			'kwaipilot/kat-coder',
			'mistralai/codestral',
			'inception/mercury-coder',
			'openrouter/free',
			'openrouter/auto',
			'openai/gpt-4o-audio-preview',
			'openai/gpt-5-image',
			'relace/relace-search',
			':thinking'
		];
	}
}
