<?php
/**
 * OpenAI Embedding Model Implementation.
 *
 * Implements vector embedding functionality using OpenAI's embedding models.
 *
 * @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;
}

use Orhanerday\OpenAi\OpenAi;

/**
 * LLM_Embedding_OpenAI class.
 */
class LLM_Embedding_OpenAI extends LLM_Embedding {
	/**
	 * Get the OpenAI client instance from the provider
	 *
	 * @return     OpenAi The OpenAI client
	 */
	private function get_client() {
		return $this->provider->get_client();
	}

	/**
	 * Generate embeddings for the provided texts using OpenAI's batch processing.
	 *
	 * This method uses OpenAI's native ability to process multiple texts in a single API request.
	 * The API supports up to 2048 inputs per request. For larger batches, use this method
	 * multiple times from the calling code.
	 *
	 * @param      array $texts Array of text strings to embed. Each string should be within
	 *                          the model's token limit (8,191 tokens for most models).
	 *                          Maximum 2048 texts per call.
	 * @return     array Array of embedding vectors in the same order as input texts.
	 *                   Each embedding is an array of floats (dimension depends on model).
	 *                   Return array length always equals input array length.
	 * @throws     \Exception If API request fails or response format is invalid.
	 * @since     1.0.0
	 */
	public function generate_embeddings( $texts ) {
		if ( ! is_array( $texts ) ) {
			$texts = array( $texts );
		}

		if ( empty( $texts ) ) {
			return array();
		}

		// Validate input limits (OpenAI API limit)
		if ( count( $texts ) > 2048 ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception( __( 'Too many texts provided. Maximum 2048 texts per call.', 'agenistrator' ) );
		}

		try {
			$client = $this->get_client();

			// Make single API request with all texts
			$response_json = $client->embeddings(
				array(
					'model' => $this->model_id,
					'input' => $texts,
				)
			);

			try {
				$response = String_Utils::decode_json( $response_json );
			} catch ( \Throwable $e ) {
				throw new \Exception(
					String_Utils::replace_tags(
						__( 'Invalid API response: {error}', 'agenistrator' ),
						array( 'error' => $e->getMessage() )
					)
				);
			}

			// Extract embeddings from the response
			if ( ! isset( $response['data'] ) || ! is_array( $response['data'] ) ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
				throw new \Exception( __( 'Invalid response structure from OpenAI embeddings API', 'agenistrator' ) );
			}

			$embeddings = array();

			foreach ( $response['data'] as $item ) {
				if ( ! isset( $item['embedding'] ) || ! is_array( $item['embedding'] ) ) {
					// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
					throw new \Exception( __( 'Invalid embedding data in OpenAI response', 'agenistrator' ) );
				}
				if ( ! isset( $item['index'] ) || $item['index'] < 0 || $item['index'] >= count( $texts ) ) {
					// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
					throw new \Exception( __( 'Invalid embedding index in OpenAI response', 'agenistrator' ) );
				}
				$embeddings[ $item['index'] ] = $item['embedding'];
			}

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

			// Verify we got the expected number of embeddings
			if ( count( $embeddings ) !== count( $texts ) ) {
				throw new \Exception(
					String_Utils::replace_tags(
						__( 'Embedding count mismatch. Expected {expected}, got {actual}.', 'agenistrator' ),
						array(
							'expected' => count( $texts ),
							'actual'   => count( $embeddings ),
						)
					)
				);
			}

			// Detect and update cached dimension if needed
			$this->detect_and_cache_dimension( $embeddings );

			return $embeddings;

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

	/**
	 * Get the dimension of embeddings for this model
	 *
	 * Auto-detects dimension on first use by making a test API call,
	 * then caches the result permanently in WordPress options.
	 *
	 * @return     int Dimension size
	 * @throws     \Exception If dimension cannot be detected
	 * @since     1.0.0
	 */
	public function get_dimension() {
		$llm_manager = LLM_Manager::get_instance();

		// Check if dimension is already cached
		$cached_dimension = $llm_manager->get_cached_embeddings_model_dimension( $this->model_id );
		if ( null !== $cached_dimension ) {
			return $cached_dimension;
		}

		// Auto-detect dimension by making a test API call
		// generate_embeddings will call detect_and_cache_dimension which caches the result
		try {
			$this->generate_embeddings( array( 'test' ) );

			// Retrieve the cached dimension that was just detected and cached
			$cached_dimension = $llm_manager->get_cached_embeddings_model_dimension( $this->model_id );
			if ( null === $cached_dimension ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
				throw new \Exception( __( 'Failed to cache dimension after detection', 'agenistrator' ) );
			}

			return $cached_dimension;

		} catch ( \Throwable $e ) {
			// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are not HTML output
			throw new \Exception(
				String_Utils::replace_tags(
					__( 'Failed to auto-detect embedding dimension for model "{model_id}": {error}', 'agenistrator' ),
					array(
						'model_id' => $this->model_id,
						'error'    => $e->getMessage(),
					)
				)
			);
			// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}
	}
}
