<?php
/**
 * Embedding Generator for BoostBot Knowledge Base
 *
 * Generates vector embeddings for semantic search.
 *
 * @package BoostBot
 * @since 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_Embedding_Generator
 *
 * Generates text embeddings using Voyage AI or OpenAI.
 */
class BoostBot_Embedding_Generator {

    /**
     * Embedding provider.
     *
     * @var string
     */
    private $provider = 'voyage';

    /**
     * Voyage AI API URL.
     *
     * @var string
     */
    private $voyage_api_url = 'https://api.voyageai.com/v1/embeddings';

    /**
     * OpenAI API URL.
     *
     * @var string
     */
    private $openai_api_url = 'https://api.openai.com/v1/embeddings';

    /**
     * API key.
     *
     * @var string
     */
    private $api_key;

    /**
     * Model to use.
     *
     * @var string
     */
    private $model;

    /**
     * Embedding dimension.
     *
     * @var int
     */
    private $dimension;

    /**
     * Maximum characters per text.
     *
     * @var int
     */
    private $max_chars = 8000;

    /**
     * Cache duration (1 month).
     *
     * @var int
     */
    private $cache_duration = MONTH_IN_SECONDS;

    /**
     * Constructor.
     *
     * @param string|null $provider Provider name (voyage, openai, or null for auto).
     */
    public function __construct( ?string $provider = null ) {
        $this->setup_provider( $provider );
    }

    /**
     * Setup embedding provider based on available API keys.
     *
     * @param string|null $provider Preferred provider.
     */
    private function setup_provider( ?string $provider ): void {
        // Get API keys from settings
        $voyage_key = BoostBot_Settings::get( 'voyage_api_key' );
        $openai_key = BoostBot_Settings::get( 'openai_api_key' );
        $backend_base = trim( (string) BoostBot_Settings::get( 'backend_base_url', '' ) );
        $backend_site_id = trim( (string) BoostBot_Settings::get( 'backend_site_id', '' ) );
        $backend_site_key = trim( (string) BoostBot_Settings::get( 'backend_site_api_key', '' ) );

        // Auto-select provider if not specified
        if ( null === $provider ) {
            if ( ! empty( $voyage_key ) ) {
                $provider = 'voyage';
            } elseif ( ! empty( $openai_key ) ) {
                $provider = 'openai';
            } elseif ( ! empty( $backend_base ) && ! empty( $backend_site_id ) && ! empty( $backend_site_key ) ) {
                $provider = 'backend';
            } else {
                // No embedding API configured - will use fallback
                $provider = 'none';
            }
        }

        $this->provider = $provider;

        switch ( $provider ) {
            case 'voyage':
                $this->api_key   = $voyage_key;
                $this->model     = 'voyage-multilingual-2'; // Good for Hebrew
                $this->dimension = 1024;
                break;

            case 'openai':
                $this->api_key   = $openai_key;
                $this->model     = 'text-embedding-3-small';
                $this->dimension = 1536;
                break;

            case 'backend':
                $this->api_key   = 'backend';
                $this->model     = 'gemini-embedding-001';
                $this->dimension = 768;
                break;

            default:
                $this->api_key   = '';
                $this->model     = 'none';
                $this->dimension = 0;
        }

        BoostBot_Logger::debug( 'Embedding generator initialized', array(
            'provider'  => $this->provider,
            'model'     => $this->model,
            'has_key'   => ! empty( $this->api_key ),
        ) );
    }

    /**
     * Check if embeddings are available.
     *
     * @return bool True if can generate embeddings.
     */
    public function is_available(): bool {
        return 'none' !== $this->provider && ! empty( $this->api_key );
    }

    /**
     * Get embedding dimension.
     *
     * @return int Dimension size.
     */
    public function get_dimension(): int {
        return $this->dimension;
    }

    /**
     * Generate embedding for text.
     *
     * @param string $text Text to embed.
     * @return array|null Embedding vector or null on failure.
     */
    public function generate( string $text ): ?array {
        if ( ! $this->is_available() ) {
            BoostBot_Logger::warning( 'Embedding generation skipped - no API configured' );
            return null;
        }

        if ( empty( $text ) ) {
            return null;
        }

        // Check cache
        $cache_key = 'boostbot_emb_' . md5( $text . $this->model );
        $cached    = get_transient( $cache_key );

        if ( false !== $cached ) {
            return $cached;
        }

        // Truncate if needed
        $text = $this->truncate( $text );

        try {
            $embedding = $this->call_api( array( $text ) );

            if ( ! empty( $embedding[0] ) ) {
                // Cache result
                set_transient( $cache_key, $embedding[0], $this->cache_duration );
                return $embedding[0];
            }
        } catch ( Exception $e ) {
            BoostBot_Logger::error( 'Embedding generation failed', array(
                'error' => $e->getMessage(),
            ) );
        }

        return null;
    }

    /**
     * Generate embeddings for multiple texts.
     *
     * @param array $texts Array of texts.
     * @return array Array of embeddings (same order as input).
     */
    public function generate_batch( array $texts ): array {
        if ( ! $this->is_available() || empty( $texts ) ) {
            return array_fill( 0, count( $texts ), null );
        }

        $results       = array();
        $uncached      = array();
        $uncached_keys = array();

        // Check cache for each text
        foreach ( $texts as $index => $text ) {
            $cache_key = 'boostbot_emb_' . md5( $text . $this->model );
            $cached    = get_transient( $cache_key );

            if ( false !== $cached ) {
                $results[ $index ] = $cached;
            } else {
                $uncached[]        = $this->truncate( $text );
                $uncached_keys[]   = array(
                    'index'     => $index,
                    'cache_key' => $cache_key,
                );
            }
        }

        // Generate embeddings for uncached texts
        if ( ! empty( $uncached ) ) {
            try {
                // API limit: ~100 texts per request
                $batches = array_chunk( $uncached, 100 );
                $key_batches = array_chunk( $uncached_keys, 100 );

                foreach ( $batches as $batch_idx => $batch ) {
                    $embeddings = $this->call_api( $batch );

                    foreach ( $embeddings as $emb_idx => $embedding ) {
                        $key_info = $key_batches[ $batch_idx ][ $emb_idx ];
                        $results[ $key_info['index'] ] = $embedding;

                        // Cache result
                        if ( null !== $embedding ) {
                            set_transient( $key_info['cache_key'], $embedding, $this->cache_duration );
                        }
                    }

                    // Rate limiting between batches
                    if ( $batch_idx < count( $batches ) - 1 ) {
                        usleep( 100000 ); // 100ms
                    }
                }
            } catch ( Exception $e ) {
                BoostBot_Logger::error( 'Batch embedding generation failed', array(
                    'error' => $e->getMessage(),
                ) );
            }
        }

        // Sort by index and return
        ksort( $results );
        return array_values( $results );
    }

    /**
     * Call embedding API.
     *
     * @param array $texts Texts to embed.
     * @return array Array of embedding vectors.
     * @throws Exception On API error.
     */
    private function call_api( array $texts ): array {
        if ( 'voyage' === $this->provider ) {
            return $this->call_voyage_api( $texts );
        } elseif ( 'openai' === $this->provider ) {
            return $this->call_openai_api( $texts );
        } elseif ( 'backend' === $this->provider ) {
            return $this->call_backend_api( $texts );
        }

        return array();
    }

    /**
     * Call backend embeddings endpoint (Option B).
     *
     * @param array $texts
     * @return array
     * @throws Exception
     */
    private function call_backend_api( array $texts ): array {
        if ( ! class_exists( 'BoostBot_Indexing_Backend_Client' ) ) {
            throw new Exception( 'Backend client not available' );
        }

        $client = new BoostBot_Indexing_Backend_Client();
        $resp = $client->embed( $texts, $this->model );

        if ( is_wp_error( $resp ) ) {
            throw new Exception( esc_html( $resp->get_error_message() ) );
        }
        if ( ! is_array( $resp ) ) {
            throw new Exception( 'Invalid backend embeddings response' );
        }

        return $resp;
    }

    /**
     * Call Voyage AI API.
     *
     * @param array $texts Texts to embed.
     * @return array Embeddings.
     * @throws Exception On error.
     */
    private function call_voyage_api( array $texts ): array {
        $response = wp_remote_post( $this->voyage_api_url, array(
            'timeout' => 60,
            'headers' => array(
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $this->api_key,
            ),
            'body'    => wp_json_encode( array(
                'input' => $texts,
                'model' => $this->model,
            ) ),
        ) );

        if ( is_wp_error( $response ) ) {
            throw new Exception( esc_html( $response->get_error_message() ) );
        }

        $code = wp_remote_retrieve_response_code( $response );
        $body = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $code >= 400 ) {
            $error = $body['detail'] ?? $body['error'] ?? 'API Error';
            throw new Exception( is_array( $error ) ? wp_json_encode( $error ) : esc_html( $error ) );
        }

        // Best-effort telemetry (Voyage may return token usage depending on API version).
        if ( isset( $body['usage'] ) || isset( $body['token_usage'] ) ) {
            BoostBot_Logger::info( 'Embeddings usage (voyage)', array(
                'model'        => $this->model,
                'texts_count'  => count( $texts ),
                'usage'        => $body['usage'] ?? null,
                'token_usage'  => $body['token_usage'] ?? null,
            ) );
        }

        $embeddings = array();
        if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
            foreach ( $body['data'] as $item ) {
                $embeddings[] = $item['embedding'] ?? null;
            }
        }

        return $embeddings;
    }

    /**
     * Call OpenAI API.
     *
     * @param array $texts Texts to embed.
     * @return array Embeddings.
     * @throws Exception On error.
     */
    private function call_openai_api( array $texts ): array {
        $response = wp_remote_post( $this->openai_api_url, array(
            'timeout' => 60,
            'headers' => array(
                'Content-Type'  => 'application/json',
                'Authorization' => 'Bearer ' . $this->api_key,
            ),
            'body'    => wp_json_encode( array(
                'input' => $texts,
                'model' => $this->model,
            ) ),
        ) );

        if ( is_wp_error( $response ) ) {
            throw new Exception( esc_html( $response->get_error_message() ) );
        }

        $code = wp_remote_retrieve_response_code( $response );
        $body = json_decode( wp_remote_retrieve_body( $response ), true );

        if ( $code >= 400 ) {
            $error = $body['error']['message'] ?? 'API Error';
            throw new Exception( esc_html( $error ) );
        }

        // Telemetry: OpenAI embeddings include usage prompt/total tokens.
        if ( isset( $body['usage'] ) && is_array( $body['usage'] ) ) {
            BoostBot_Logger::info( 'Embeddings usage (openai)', array(
                'model'       => $this->model,
                'texts_count' => count( $texts ),
                'usage'       => $body['usage'],
            ) );
        }

        $embeddings = array();
        if ( isset( $body['data'] ) && is_array( $body['data'] ) ) {
            // Sort by index to maintain order
            usort( $body['data'], function( $a, $b ) {
                return ( $a['index'] ?? 0 ) - ( $b['index'] ?? 0 );
            } );

            foreach ( $body['data'] as $item ) {
                $embeddings[] = $item['embedding'] ?? null;
            }
        }

        return $embeddings;
    }

    /**
     * Truncate text to maximum length.
     *
     * @param string $text Text to truncate.
     * @return string Truncated text.
     */
    private function truncate( string $text ): string {
        if ( mb_strlen( $text ) <= $this->max_chars ) {
            return $text;
        }

        return mb_substr( $text, 0, $this->max_chars );
    }

    /**
     * Calculate cosine similarity between two vectors.
     *
     * @param array $vec1 First vector.
     * @param array $vec2 Second vector.
     * @return float Similarity score (0-1).
     */
    public static function cosine_similarity( array $vec1, array $vec2 ): float {
        if ( count( $vec1 ) !== count( $vec2 ) || empty( $vec1 ) ) {
            return 0.0;
        }

        $dot_product = 0.0;
        $norm1       = 0.0;
        $norm2       = 0.0;

        $count = count( $vec1 );
        for ( $i = 0; $i < $count; $i++ ) {
            $dot_product += $vec1[ $i ] * $vec2[ $i ];
            $norm1       += $vec1[ $i ] * $vec1[ $i ];
            $norm2       += $vec2[ $i ] * $vec2[ $i ];
        }

        if ( 0.0 === $norm1 || 0.0 === $norm2 ) {
            return 0.0;
        }

        return $dot_product / ( sqrt( $norm1 ) * sqrt( $norm2 ) );
    }

    /**
     * Get current provider info.
     *
     * @return array Provider information.
     */
    public function get_provider_info(): array {
        return array(
            'provider'  => $this->provider,
            'model'     => $this->model,
            'dimension' => $this->dimension,
            'available' => $this->is_available(),
        );
    }
}








