<?php
/**
 * Costudio_Gemini_Adapter — Google Gemini API adapter.
 *
 * Implements native Google Search Grounding when enabled; automatically falls
 * back to plain generation when grounding is unavailable (free tier / billing).
 *
 * @package Contentosapp_Studio
 * @since   3.0.0
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages go to PHP error log, never to HTML output.

class Costudio_Gemini_Adapter implements Costudio_AI_Client {

    const API_BASE      = 'https://generativelanguage.googleapis.com/v1beta/models/';
    const DEFAULT_MODEL = 'gemini-2.5-flash';

    private string $api_key;
    private string $model;
    private bool   $grounding_enabled;
    private int    $thinking_budget = -1;
    private ?float $temperature     = null;

    /** Set to true after one fallback retry to prevent infinite loops. */
    private bool $fallback_attempted = false;

    /** Durable-worker hooks. They are unset for the existing synchronous path. */
    private $before_dispatch_callback = null;
    private $response_received_callback = null;
    private ?int $http_timeout = null;
    private bool $model_fallback_enabled = true;

    /** @var array Last API call usage metadata. */
    private array $last_usage = [ 'input_tokens' => 0, 'output_tokens' => 0 ];

    /** @var array<int,array{url:string,title:string,snippet:string}> Sources from the last grounded call. */
    private array $last_grounding_sources = [];

    /**
     * @param string $api_key
     * @param string $model              e.g. 'gemini-2.5-flash'
     * @param bool   $grounding_enabled  Whether to use native Google Search Grounding.
     */
    public function __construct(
        string $api_key,
        string $model = 'gemini-2.5-flash',
        bool $grounding_enabled = true
    ) {
        $this->api_key           = $api_key;
        $this->model             = $model;
        $this->grounding_enabled = $grounding_enabled;
    }

    // ── Public interface ─────────────────────────────────────────────────────

    public function generate( string $prompt, array $context = [] ): string {
        return $this->call_api( $prompt, $context, false );
    }

    public function generate_via_agent(int $agent_id, array $inputs, array $context = []): string
    {
        throw new LogicException('Secure agent execution is only available via ContentOS Cloud adapter.');
    }

    /**
     * Generate with Google Search Grounding.
     *
     * Grounding requires billing on the Google Cloud project; on the free tier
     * the API returns empty candidates.  This method catches that and retries
     * without grounding so the pipeline never stalls.
     */
    public function generateWithSearch( string $prompt, array $context = [] ): string {
        if ( ! $this->grounding_enabled ) {
            return $this->call_api( $prompt, $context, false );
        }

        try {
            return $this->call_api( $prompt, $context, true );
        } catch ( Costudio_Grounding_Exception $e ) {
            $no_grounding = self::get_no_grounding_instruction();
            return $this->call_api( $no_grounding . "\n\n" . $prompt, $context, false );
        }
    }

    public function get_model(): string {
        return $this->model;
    }

    /**
     * Set the thinking budget for Gemini 2.5 models.
     *
     * @param int $budget Token budget for thinking (-1 = disabled/default).
     */
    public function set_thinking_budget( int $budget ): void {
        $this->thinking_budget = $budget;
    }

    public function set_temperature( ?float $temperature ): void {
        $this->temperature = $temperature;
    }

    public function set_durable_transport_hooks( callable $before_dispatch, callable $response_received ): void {
        $this->before_dispatch_callback  = $before_dispatch;
        $this->response_received_callback = $response_received;
    }

    public function set_http_timeout( int $seconds ): void {
        $this->http_timeout = max( 1, $seconds );
    }

    public function disable_model_fallback(): void {
        $this->model_fallback_enabled = false;
    }

    /**
     * Get token usage from the last API call.
     *
     * @return array { input_tokens: int, output_tokens: int }
     */
    public function get_last_usage(): array {
        return $this->last_usage;
    }

    /**
     * Structured grounding sources from the last grounded call (Google Search
     * Grounding). URLs are Vertex grounding-redirects; the caller resolves them.
     * Empty when grounding didn't fire (free tier / no billing).
     *
     * @return array<int,array{url:string,title:string,snippet:string}>
     */
    public function get_last_grounding_sources(): array {
        return $this->last_grounding_sources;
    }

    // ── Internal ─────────────────────────────────────────────────────────────

    private static function get_no_grounding_instruction(): string {
        return '[MODO SEM GROUNDING] Esta pesquisa está sendo feita SEM acesso à web em tempo real. '
            . 'Siga estritamente as regras da seção "Modo sem grounding" do seu prompt: seja conservador com dados, '
            . 'evite números ou datas específicos sem fonte no contexto, não invente links e indique onde revisão manual é recomendada.';
    }

    private function call_api( string $prompt, array $context, bool $with_search ): string {
        // Reset per call so a later non-grounded retry can't leak stale sources.
        $this->last_grounding_sources = [];

        $url = self::API_BASE . rawurlencode( $this->model ) . ':generateContent?key=' . urlencode( $this->api_key );

        $gen_config = [
            'temperature'     => $this->temperature ?? 0.7,
            'maxOutputTokens' => 8192,
        ];

        // Gemini 2.5 models support thinkingConfig for extended reasoning.
        if ( $this->thinking_budget > 0 && strpos( $this->model, '2.5' ) !== false ) {
            $gen_config['thinkingConfig'] = [
                'thinkingBudget' => $this->thinking_budget,
            ];
        }

        $body = [
            'contents'         => $this->build_contents( $prompt, $context ),
            'generationConfig' => $gen_config,
        ];

        if ( $with_search ) {
            $body['tools'] = [ [ 'google_search' => (object) [] ] ];
        }

        $timeout = null !== $this->http_timeout
            ? $this->http_timeout
            : (int) apply_filters( 'costudio_gemini_http_timeout', 300 );

        costudio_log_debug( 'gemini.call_starting', [
            'model'   => $this->model,
            'has_key' => ! empty( $this->api_key ),
            'search'  => $with_search ? 'yes' : 'no',
            'timeout' => $timeout,
        ] );

        $this->notify_before_dispatch();
        $response = wp_remote_post( $url, [
            'timeout' => $timeout,
            'headers' => [ 'Content-Type' => 'application/json' ],
            'body'    => wp_json_encode( $body ),
        ] );

        if ( is_wp_error( $response ) ) {
            costudio_log( 'gemini.call_failed', [ 'error' => $response->get_error_message() ], 'warning' );
        } else {
            $this->notify_response_received();
            costudio_log_debug( 'gemini.call_returned', [
                'http'  => wp_remote_retrieve_response_code( $response ),
                'model' => $this->model,
            ] );
        }

        // Deprecated/unknown model fallback — inspect the raw response BEFORE
        // parse_response() so we can swap the model and retry once.
        if ( ! is_wp_error( $response ) && $this->model_fallback_enabled && ! $this->fallback_attempted ) {
            $code = (int) wp_remote_retrieve_response_code( $response );
            if ( $code !== 200 ) {
                $raw  = (string) wp_remote_retrieve_body( $response );
                $data = json_decode( $raw, true );
                $msg  = is_array( $data ) ? ( $data['error']['message'] ?? $raw ) : $raw;

                if ( $this->model !== self::DEFAULT_MODEL
                     && class_exists( 'Costudio_Model_Fallback' )
                     && Costudio_Model_Fallback::is_deprecated_model_error( $code, (string) $msg ) ) {

                    $this->fallback_attempted = true;
                    Costudio_Model_Fallback::record( 'gemini', $this->model, self::DEFAULT_MODEL, (string) $msg );
                    $this->model = self::DEFAULT_MODEL;
                    return $this->call_api( $prompt, $context, $with_search );
                }
            }
        }

        return $this->parse_response( $response, $with_search );
    }

    private function build_contents( string $prompt, array $context ): array {
        $contents = [];

        foreach ( $context as $turn ) {
            $role       = ( $turn['role'] ?? 'user' ) === 'model' ? 'model' : 'user';
            $contents[] = [
                'role'  => $role,
                'parts' => [ [ 'text' => $turn['content'] ] ],
            ];
        }

        $contents[] = [
            'role'  => 'user',
            'parts' => [ [ 'text' => $prompt ] ],
        ];

        return $contents;
    }

    private function parse_response( $response, bool $with_search = false ): string {
        if ( is_wp_error( $response ) ) {
            throw new Exception( __( 'Gemini API error:', 'contentosapp-studio' ) . ' ' . $response->get_error_message() );
        }

        $code = (int) wp_remote_retrieve_response_code( $response );
        $raw  = wp_remote_retrieve_body( $response );
        $data = json_decode( $raw, true );

        if ( $code !== 200 ) {
            $msg = $data['error']['message'] ?? $raw;
            throw new Exception( "Gemini API HTTP {$code}: {$msg}" );
        }

        if ( ! empty( $data['promptFeedback']['blockReason'] ) ) {
            $reason = $data['promptFeedback']['blockReason'];
            throw new Exception(
                sprintf(
                    /* translators: %s: Gemini block reason */
                    __( 'Gemini blocked the prompt (blockReason: %s). Review the prompt or disable safety filters in Google AI Studio.', 'contentosapp-studio' ),
                    $reason
                )
            );
        }

        if ( empty( $data['candidates'] ) ) {
            if ( $with_search ) {
                throw new Costudio_Grounding_Exception(
                    __( 'Google Search Grounding returned empty. This feature requires active billing. The plugin will retry without web search.', 'contentosapp-studio' )
                );
            }
            throw new Exception(
                __( 'Gemini returned a response without candidates. Verify that the API key is valid and quota is available.', 'contentosapp-studio' )
            );
        }

        // Extract token usage from usageMetadata.
        if ( ! empty( $data['usageMetadata'] ) ) {
            $this->last_usage = [
                'input_tokens'  => (int) ( $data['usageMetadata']['promptTokenCount'] ?? 0 ),
                'output_tokens' => (int) ( $data['usageMetadata']['candidatesTokenCount'] ?? 0 ),
            ];
        }

        $candidate     = $data['candidates'][0];
        $finish_reason = $candidate['finishReason'] ?? 'STOP';

        // Capture structured grounding sources (Google Search Grounding). The URLs
        // are Vertex grounding-redirects; the Gemini engine resolves them before
        // they reach the citable {{ref:N}} pool. Empty when grounding didn't fire.
        foreach ( ( $candidate['groundingMetadata']['groundingChunks'] ?? [] ) as $chunk ) {
            $uri = isset( $chunk['web']['uri'] ) ? trim( (string) $chunk['web']['uri'] ) : '';
            if ( '' === $uri ) {
                continue;
            }
            $this->last_grounding_sources[] = [
                'url'     => $uri,
                'title'   => isset( $chunk['web']['title'] ) ? (string) $chunk['web']['title'] : '',
                'snippet' => '',
            ];
        }

        $blocked = [ 'SAFETY', 'RECITATION', 'PROHIBITED_CONTENT', 'SPII' ];
        if ( in_array( $finish_reason, $blocked, true ) ) {
            throw new Exception(
                sprintf(
                    /* translators: %s: Gemini finish reason */
                    __( 'Gemini blocked the response (finishReason: %s). Try reformulating the agent prompt in Settings -> Agents.', 'contentosapp-studio' ),
                    $finish_reason
                )
            );
        }

        $text = '';
        foreach ( ( $candidate['content']['parts'] ?? [] ) as $part ) {
            if ( isset( $part['text'] ) ) {
                $text .= $part['text'];
            }
        }

        if ( empty( $text ) ) {
            $hint = sprintf(
                /* translators: %s: Gemini finish reason */
                __( 'Gemini returned an empty response (finishReason: %s).', 'contentosapp-studio' ),
                $finish_reason
            );

            if ( $with_search ) {
                throw new Costudio_Grounding_Exception( $hint );
            }

            if ( $finish_reason === 'MAX_TOKENS' ) {
                throw new Exception(
                    __( 'The response reached the token limit without generating text. Reduce requested length or use model gemini-2.5-pro.', 'contentosapp-studio' )
                );
            }

            throw new Exception( $hint . ' Verifique sua API key e a quota disponível.' );
        }

        return $text;
    }

    public function generate_streaming( string $prompt, array $context, callable $on_chunk, bool $with_search = false ): string {
        $result = $this->call_api( $prompt, $context, $with_search );
        $on_chunk( $result );
        return $result;
    }

    private function notify_before_dispatch(): void {
        if ( is_callable( $this->before_dispatch_callback ) ) {
            call_user_func( $this->before_dispatch_callback );
        }
    }

    private function notify_response_received(): void {
        if ( is_callable( $this->response_received_callback ) ) {
            call_user_func( $this->response_received_callback );
        }
    }
}
