<?php
if (count(get_included_files()) === 1) { http_response_code(403); exit('Forbidden'); }

/**
 * GeminiService — cliente centralizado para la API de Gemini (Google).
 *
 * Todos los accesos a Gemini pasan por acá. Soporta:
 *  - Texto (modo normal, function calling)
 *  - Multimodal (imágenes via inlineData o fileUri)
 *  - Google Search grounding (búsqueda en tiempo real)
 *  - Selección de modelo por tarea
 *  - Retry automático en errores transitorios (429, 503)
 *  - Fallback de modelo (Pro → Flash si Pro falla)
 */
class GeminiService {

    // Modelos disponibles
    const MODEL_FLASH  = 'gemini-2.0-flash';
    const MODEL_PRO    = 'gemini-1.5-pro';
    const MODEL_FLASH8 = 'gemini-1.5-flash-8b'; // ultra-rápido para tasks simples

    // Timeouts
    const TIMEOUT_CHAT     = 30; // segundos — respuesta de chat
    const TIMEOUT_ANALYSIS = 60; // segundos — análisis profundo con Pro

    // Reintentos en errores transitorios
    const MAX_RETRIES = 2;
    const RETRY_DELAY_MS = 800; // milisegundos entre reintentos

    /**
     * Llamada estándar: texto + function calling opcional.
     * Backward-compatible con el contrato anterior.
     *
     * @param string     $systemInstruction  System prompt completo
     * @param array      $contents           Historial en formato Gemini
     * @param array|null $tools              Function declarations (ToolRegistry)
     * @param string|null $model             Modelo a usar (default: env GEMINI_MODEL o flash)
     * @param array       $options           Opciones extra: grounding, responseFormat, temperature
     *
     * @return array{text:string, functionCalls:array, candidate:?array, tokens:?int,
     *               groundingSources:array, raw_request:array, raw_response:array}
     * @throws Exception si la llamada falla después de reintentos
     */
    public static function generate(
        string $systemInstruction,
        array  $contents,
        ?array $tools   = null,
        ?string $model  = null,
        array  $options = []
    ): array {
        $apiKey = getenv('GEMINI_API_KEY');
        if (empty($apiKey)) {
            throw new Exception('GEMINI_API_KEY no configurada.');
        }

        // Modelo: parámetro > env > fallback
        $model = $model ?: (getenv('GEMINI_MODEL') ?: self::MODEL_FLASH);
        $timeout = $options['timeout'] ?? self::TIMEOUT_CHAT;

        $body = self::buildRequestBody($systemInstruction, $contents, $tools, $options);
        $url  = self::endpointUrl($model, $apiKey);

        return self::callWithRetry($url, $body, $timeout, $model, $apiKey, $options);
    }

    /**
     * Llamada con Google Search grounding activo.
     * Vale puede buscar en Google en tiempo real y usar los resultados como contexto.
     * Útil para: diagnóstico de presencia digital, verificar datos del negocio.
     */
    public static function generateWithSearch(
        string $systemInstruction,
        array  $contents,
        ?string $model = null
    ): array {
        return self::generate($systemInstruction, $contents, null, $model, [
            'grounding' => true,
            'timeout'   => self::TIMEOUT_ANALYSIS,
        ]);
    }

    /**
     * Llamada con modelo Pro para análisis profundos (diagnóstico multimodal, documentos).
     * Más lento y costoso — usar solo cuando la calidad lo justifica.
     */
    public static function generatePro(
        string $systemInstruction,
        array  $contents,
        ?array $tools = null,
        array  $options = []
    ): array {
        $options['timeout'] = self::TIMEOUT_ANALYSIS;
        return self::generate($systemInstruction, $contents, $tools, self::MODEL_PRO, $options);
    }

    /**
     * Construye una "part" de imagen para incluir en $contents.
     * El caller ensambla el contenido así:
     *
     *   $contents[] = ['role' => 'user', 'parts' => [
     *       ['text' => 'Descripción de lo que quiero analizar'],
     *       GeminiService::imagePart($base64, 'image/jpeg'),
     *   ]];
     *
     * @param string $base64   Imagen en base64
     * @param string $mimeType 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif'
     */
    public static function imagePart(string $base64, string $mimeType = 'image/jpeg'): array {
        return ['inlineData' => ['mimeType' => $mimeType, 'data' => $base64]];
    }

    /**
     * Construye una "part" de imagen desde una URL pública.
     * Gemini descarga y analiza la imagen en el servidor de Google.
     *
     * @param string $url      URL pública de la imagen
     * @param string $mimeType Tipo MIME de la imagen
     */
    public static function imagePartFromUrl(string $url, string $mimeType = 'image/jpeg'): array {
        return ['fileData' => ['mimeType' => $mimeType, 'fileUri' => $url]];
    }

    /**
     * Descarga una imagen desde una URL y devuelve su base64.
     * Útil para imágenes de WhatsApp (media_url de Meta/Twilio).
     *
     * @throws Exception si la descarga falla
     */
    public static function downloadImageAsBase64(string $url, int $timeoutSec = 10): array {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_TIMEOUT        => $timeoutSec,
            CURLOPT_USERAGENT      => 'ValeAI/1.0',
        ]);
        $data     = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $mime     = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?: 'image/jpeg';
        $error    = curl_error($ch);
        curl_close($ch);

        if ($data === false || $httpCode !== 200) {
            throw new Exception("No se pudo descargar la imagen (HTTP $httpCode): $error");
        }

        // Normalizar mimeType (puede venir con charset o parámetros extra)
        $mime = strtok($mime, ';');

        return [
            'base64'   => base64_encode($data),
            'mimeType' => trim($mime) ?: 'image/jpeg',
        ];
    }

    // ── Internals ─────────────────────────────────────────────────────────────

    private static function buildRequestBody(
        string $systemInstruction,
        array  $contents,
        ?array $tools,
        array  $options
    ): array {
        $genConfig = [
            'temperature'    => $options['temperature'] ?? 0.3,
            'maxOutputTokens'=> $options['maxOutputTokens'] ?? 2048,
        ];
        if (isset($options['thinkingBudget'])) {
            $genConfig['thinkingConfig'] = ['thinkingBudget' => (int) $options['thinkingBudget']];
        }
        // PHP json_decode(…, true) convierte {} vacío a [] (array), que json_encode devuelve
        // como lista []. Gemini exige que functionCall.args sea siempre un objeto {}.
        foreach ($contents as &$turn) {
            if (empty($turn['parts'])) continue;
            foreach ($turn['parts'] as &$part) {
                if (isset($part['functionCall']['args']) && $part['functionCall']['args'] === []) {
                    $part['functionCall']['args'] = new stdClass();
                }
            }
            unset($part);
        }
        unset($turn);

        $body = [
            'contents'          => $contents,
            'systemInstruction' => ['parts' => [['text' => $systemInstruction]]],
            'generationConfig'  => $genConfig,
        ];

        // JSON mode — cuando se espera una respuesta estructurada
        if (!empty($options['responseFormat']) && $options['responseFormat'] === 'json') {
            $body['generationConfig']['responseMimeType'] = 'application/json';
        }

        // Function declarations
        $toolsArray = [];
        if (!empty($tools)) {
            $toolsArray[] = ['functionDeclarations' => $tools];
        }

        // Google Search grounding
        if (!empty($options['grounding'])) {
            $toolsArray[] = ['googleSearch' => new stdClass()];
        }

        if (!empty($toolsArray)) {
            $body['tools'] = $toolsArray;
        }

        return $body;
    }

    private static function endpointUrl(string $model, string $apiKey): string {
        return "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
    }

    private static function callWithRetry(
        string $url,
        array  $body,
        int    $timeout,
        string $model,
        string $apiKey,
        array  $options
    ): array {
        $lastException = null;

        for ($attempt = 0; $attempt <= self::MAX_RETRIES; $attempt++) {
            if ($attempt > 0) {
                usleep(self::RETRY_DELAY_MS * 1000 * $attempt); // back-off progresivo
            }

            try {
                return self::doRequest($url, $body, $timeout);
            } catch (Exception $e) {
                $lastException = $e;
                $msg = $e->getMessage();

                // Reintentar solo en errores transitorios
                if (!self::isRetryable($msg)) {
                    break;
                }

                // En el último intento con Pro, fallback a Flash si está habilitado
                if ($attempt === self::MAX_RETRIES
                    && strpos($model, 'pro') !== false
                    && !empty($options['proFallbackToFlash'])
                ) {
                    error_log("[GeminiService] Pro falló, fallback a Flash: " . $msg);
                    $url = self::endpointUrl(self::MODEL_FLASH, $apiKey);
                }
            }
        }

        throw new Exception(
            "Gemini no disponible después de " . (self::MAX_RETRIES + 1) . " intentos. Último error: "
            . ($lastException ? $lastException->getMessage() : 'desconocido')
        );
    }

    private static function doRequest(string $url, array $body, int $timeout): array {
        $jsonBody = json_encode($body, JSON_UNESCAPED_UNICODE);

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $jsonBody,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Content-Length: ' . strlen($jsonBody),
            ],
            CURLOPT_TIMEOUT        => $timeout,
        ]);

        $response  = curl_exec($ch);
        $httpCode  = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($response === false) {
            throw new Exception("cURL error: $curlError");
        }

        $decoded = json_decode($response, true);
        if ($httpCode !== 200) {
            $msg = $decoded['error']['message'] ?? "HTTP $httpCode: $response";
            throw new Exception("Gemini API error: $msg");
        }

        return self::parseResponse($decoded, $body);
    }

    private static function parseResponse(array $decoded, array $rawRequest): array {
        $candidate = $decoded['candidates'][0]['content'] ?? null;
        $parts     = $candidate['parts'] ?? [];

        $text          = '';
        $functionCalls = [];

        foreach ($parts as $part) {
            if (isset($part['text'])) {
                $text .= $part['text'];
            }
            if (isset($part['functionCall'])) {
                $functionCalls[] = [
                    'id'   => $part['functionCall']['id']   ?? null,
                    'name' => $part['functionCall']['name'] ?? '',
                    'args' => $part['functionCall']['args'] ?? [],
                ];
            }
        }

        // Fuentes del grounding (cuando se usó Google Search)
        $groundingSources = [];
        $groundingMeta = $decoded['candidates'][0]['groundingMetadata'] ?? [];
        if (!empty($groundingMeta['groundingChunks'])) {
            foreach ($groundingMeta['groundingChunks'] as $chunk) {
                if (!empty($chunk['web']['uri'])) {
                    $groundingSources[] = [
                        'url'   => $chunk['web']['uri'],
                        'title' => $chunk['web']['title'] ?? '',
                    ];
                }
            }
        }

        return [
            'text'             => trim($text),
            'functionCalls'    => $functionCalls,
            'candidate'        => $candidate,
            'tokens'           => $decoded['usageMetadata']['totalTokenCount'] ?? null,
            'groundingSources' => $groundingSources,
            'raw_request'      => $rawRequest,
            'raw_response'     => $decoded,
        ];
    }

    private static function isRetryable(string $errorMessage): bool {
        $retryablePatterns = ['429', '503', '500', 'timeout', 'timed out', 'temporarily'];
        foreach ($retryablePatterns as $pattern) {
            if (stripos($errorMessage, $pattern) !== false) {
                return true;
            }
        }
        return false;
    }
}
