<?php
/**
 * LLM Manager.
 *
 * Provides utility methods for managing LLMs. Handles model selection,
 * configuration, and creation.
 *
 * @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;
}

/**
 * LLM_Manager class.
 */
class LLM_Manager extends Manager {
	/**
	 * Constructor - initializes the manager
	 * @since     1.0.0
	 */
	protected function __construct() {
		// Call parent constructor with option suffix
		parent::__construct( 'llm_settings' );
	}

	/**
	 * Get default configuration
	 *
	 * Returns default LLM settings values.
	 *
	 * @return array Default configuration
	 * @since     1.0.0
	 */
	protected function get_default_config() {
		// Return default configuration
		return array(
			'chat_model'                  => '',
			'embeddings_model'            => '',
			'embeddings_model_dimensions' => array(), // Cache for embeddings model dimensions keyed by model_id
		);
	}

	/**
	 * Uninstall plugin
	 *
	 * Clears model info cache transient and removes configuration.
	 * @since     1.0.0
	 */
	public function uninstall_plugin() {
		// Clear model info dev cache transient
		Model_Info::get_instance()->clear_cache();

		// Call parent to delete option
		parent::uninstall_plugin();
	}

	/**
	 * Check if a chat model is selected
	 *
	 * @return     bool Whether a chat model is selected and its provider exists
	 * @since     1.0.0
	 */
	public function is_chat_model_configured() {
		try {
			// Check if a chat model is selected
			$chat_model = $this->get_chat_model_id();
			if ( empty( $chat_model ) ) {
				return false;
			}

			// Check if the provider for this model exists
			$provider = $this->get_provider_for_model( $chat_model );
			return ( null !== $provider );
		} catch ( \Throwable $e ) {
			return false;
		}
	}

	/**
	 * Check if an embeddings model is selected
	 *
	 * @return     bool Whether an embeddings model is selected and its provider exists
	 * @since     1.0.0
	 */
	public function is_embeddings_model_configured() {
		try {
			// Check if an embeddings model is selected
			$embeddings_model = $this->get_embeddings_model_id();
			if ( empty( $embeddings_model ) ) {
				return false;
			}

			// Check if the provider for this model exists
			$provider = $this->get_provider_for_model( $embeddings_model );
			return ( null !== $provider );
		} catch ( \Throwable $e ) {
			return false;
		}
	}

	/**
	 * Get provider for a namespaced model ID
	 *
	 * Parses the namespace prefix (e.g., 'openai/gpt-4o') to determine the provider type,
	 * then searches only providers of that type. When multiple providers of the same type
	 * support the model, one is selected randomly to distribute load.
	 *
	 * @param      string $namespaced_model_id The namespaced model ID (e.g., 'openai/gpt-4o')
	 * @return     Provider_Type|null Provider instance or null if not found
	 * @since     1.0.0
	 */
	public function get_provider_for_model( $namespaced_model_id ) {
		// Extract provider type from namespace prefix (e.g., "openai/gpt-4o" or "openrouter/openai/gpt-4o").
		$parts = explode( '/', $namespaced_model_id, 2 );
		if ( count( $parts ) !== 2 || empty( $parts[0] ) || empty( $parts[1] ) ) {
			return null;
		}

		$type_id          = $parts[0];
		$raw_model_id     = $parts[1];
		$provider_manager = Provider_Manager::get_instance();
		$providers        = $provider_manager->get_config( array( 'providers' ), array() );
		$matching         = array();

		foreach ( $providers as $provider_id => $config ) {
			// Skip disabled providers or wrong type.
			if ( empty( $config['enabled'] ) || ( $config['type'] ?? '' ) !== $type_id ) {
				continue;
			}

			try {
				$provider = $provider_manager->get_provider( $provider_id );
				// Check if the provider has all required parameters set and has the model
				if ( $provider->is_configured() && $provider->has_model( $raw_model_id ) ) {
					$matching[] = $provider;
				}
			} catch ( \Throwable $e ) {
				continue;
			}
		}

		return empty( $matching ) ? null : $matching[ array_rand( $matching ) ];
	}

	/**
	 * Strip the namespace prefix from a namespaced model ID
	 *
	 * @param      string $namespaced_model_id The namespaced model ID (e.g., 'openai/gpt-4o')
	 * @return     string The raw model ID (e.g., 'gpt-4o'), or original if no namespace
	 * @since     1.0.0
	 */
	public function strip_model_namespace( $namespaced_model_id ) {
		$parts = explode( '/', $namespaced_model_id, 2 );
		return count( $parts ) === 2 ? $parts[1] : $namespaced_model_id;
	}

	/**
	 * Create a namespaced model ID by prefixing with provider type
	 *
	 * @param      Provider_Type $provider     The provider instance
	 * @param      string        $raw_model_id The raw model ID (e.g., 'gpt-4o')
	 * @return     string Namespaced model ID (e.g., 'openai/gpt-4o')
	 * @since     1.0.0
	 */
	public function namespace_model_id( $provider, $raw_model_id ) {
		return $provider->get_type_id() . '/' . $raw_model_id;
	}

	/**
	 * Get a chat model instance using the configured model
	 *
	 * @return     LLM_Chat Chat model instance
	 * @throws     \Exception If no chat model is configured or provider not found
	 * @since     1.0.0
	 */
	public function get_chat_model() {
		$namespaced_model_id = $this->get_chat_model_id();

		if ( empty( $namespaced_model_id ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'No chat model configured', 'agenistrator' ) );
		}

		$provider = $this->get_provider_for_model( $namespaced_model_id );

		if ( ! $provider ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception messages are not HTML output
			throw new \Exception(
				String_Utils::replace_tags(
					__( 'No provider found for chat model {model_id}', 'agenistrator' ),
					array( 'model_id' => $namespaced_model_id )
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// Strip namespace prefix before passing to provider API
		$raw_model_id = $this->strip_model_namespace( $namespaced_model_id );
		return $provider->create_chat_model( $raw_model_id );
	}

	/**
	 * Get an embedding model instance using the configured model
	 *
	 * @return     LLM_Embedding Embedding model instance
	 * @throws     \Exception If no embeddings model is configured or provider not found
	 * @since     1.0.0
	 */
	public function get_embedding_model() {
		$namespaced_model_id = $this->get_embeddings_model_id();

		if ( empty( $namespaced_model_id ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'No embeddings model configured', 'agenistrator' ) );
		}

		$provider = $this->get_provider_for_model( $namespaced_model_id );

		if ( ! $provider ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- exception messages are not HTML output
			throw new \Exception(
				String_Utils::replace_tags(
					__( 'No provider found for embeddings model {model_id}', 'agenistrator' ),
					array( 'model_id' => $namespaced_model_id )
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		// Strip namespace prefix before passing to provider API
		$raw_model_id = $this->strip_model_namespace( $namespaced_model_id );
		return $provider->create_embedding_model( $raw_model_id );
	}

	/**
	 * Initialize default chat model for a newly configured provider
	 *
	 * Sets default chat model if none is currently configured.
	 * Embeddings model is not auto-set to avoid unexpected costs from
	 * background embedding generation.
	 * Should be called after creating and enabling a new provider.
	 *
	 * @param      Provider_Type $provider The provider to get defaults from
	 * @return     void
	 * @since     1.0.0
	 */
	public function initialize_default_models_for_provider( $provider ) {
		// Set default chat model if none configured
		if ( empty( $this->get_chat_model_id() ) ) {
			$default_chat = $provider->get_default_chat_model();
			if ( null !== $default_chat ) {
				$namespaced = $this->namespace_model_id( $provider, $default_chat );
				$this->set_chat_model_id( $namespaced );
			}
		}
	}

	/**
	 * Get the configured chat model ID
	 *
	 * @return     string The namespaced chat model ID (e.g., 'openai/gpt-4o')
	 * @since     1.0.0
	 */
	public function get_chat_model_id() {
		return $this->get_config( 'chat_model', '' );
	}

	/**
	 * Set the chat model ID
	 *
	 * @param      string $model_id The namespaced model ID to set (e.g., 'openai/gpt-4o')
	 * @return     bool Whether the option was updated
	 * @since     1.0.0
	 */
	public function set_chat_model_id( $model_id ) {
		// Update the model ID in the config and save
		return $this->set_config_and_save( 'chat_model', $model_id );
	}

	/**
	 * Get the configured embeddings model ID
	 *
	 * @return     string The namespaced embeddings model ID (e.g., 'openai/text-embedding-3-small')
	 * @since     1.0.0
	 */
	public function get_embeddings_model_id() {
		return $this->get_config( 'embeddings_model', '' );
	}

	/**
	 * Set the embeddings model ID
	 *
	 * Cancels any in-progress embeddings generation if the model is changing,
	 * since embeddings generated with the old model would be incompatible.
	 * Schedules new embeddings generation when a model is set.
	 *
	 * @param      string $model_id The model ID to set
	 * @return     bool Whether the option was updated
	 * @since     1.0.0
	 */
	public function set_embeddings_model_id( $model_id ) {
		$old_model     = $this->get_config( 'embeddings_model', '' );
		$model_changed = $old_model !== $model_id;

		// Cancel any in-progress embeddings generation if model is changing
		if ( '' !== $old_model && $model_changed ) {
			Embeddings_Manager::get_instance()->cancel_embeddings_generation();
		}

		$result = $this->set_config_and_save( 'embeddings_model', $model_id );

		// Schedule embeddings regeneration if model is set and different from old
		if ( '' !== $model_id && $model_changed ) {
			try {
				Embeddings_Manager::get_instance()->schedule_sync_content_embeddings( true, false, false );
			} catch ( \Throwable $e ) {
				// Log error but don't fail the model change
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Production error logging for embeddings scheduling failures
				error_log( 'Agenistrator: Failed to schedule embeddings regeneration: ' . $e->getMessage() );
			}
		}

		return $result;
	}

	/**
	 * Get cached dimension for an embeddings model
	 *
	 * @param      string $model_id The embeddings model ID
	 * @return     int|null The cached dimension or null if not cached
	 * @since     1.0.0
	 */
	public function get_cached_embeddings_model_dimension( $model_id ) {
		$dimensions = $this->get_config( 'embeddings_model_dimensions', array() );
		return $dimensions[ $model_id ] ?? null;
	}

	/**
	 * Cache dimension for an embeddings model
	 *
	 * @param      string $model_id The embeddings model ID
	 * @param      int $dimension The dimension to cache
	 * @return     bool Whether the cache was saved successfully
	 * @since     1.0.0
	 */
	public function cache_embeddings_model_dimension( $model_id, $dimension ) {
		$dimensions              = $this->get_config( 'embeddings_model_dimensions', array() );
		$dimensions[ $model_id ] = $dimension;
		return $this->set_config_and_save( 'embeddings_model_dimensions', $dimensions );
	}
}
