<?php
/**
 * RBC AI Provider Engine
 * Cascading fallback: Claude → Groq → Gemini → OpenAI
 *
 * Single entry point for ALL AI text generation across EduTrack.
 * Every module that needs AI calls this file — never calls a provider directly.
 */

if (!defined('DB_HOST')) {
    require_once __DIR__ . '/../config.php';
}

class RBCAIEngine {

    // Groq first — cheapest and fastest of the funded engines. Claude Haiku is
    // the mid fallback, OpenAI next, Claude Sonnet the quality safety net for
    // the hardest papers and diagrams. Gemini sits dead last: its free tier
    // reports `limit: 0`, so it is a fallback of last resort rather than a
    // first choice. Do NOT put Opus-tier Claude in the hot path.
    private static $providers = ['groq', 'claude', 'openai', 'claude_sonnet', 'gemini'];

    // Network budget per provider. Raised from 10-30s: a 2000-token JSON reply
    // routinely takes longer than 10s, so the old cap was aborting responses
    // that were about to succeed. Kept modest so a full cascade stays bounded —
    // the cooldown below is what stops a dead provider costing this every call.
    private const HTTP_TIMEOUT         = 45;
    private const HTTP_CONNECT_TIMEOUT = 8;

    // How long to skip a provider that just failed with a hard, non-transient
    // error (quota exhausted, bad key, unreachable host). Keeps the configured
    // cascade ORDER intact while not paying its timeout on every request.
    private const COOLDOWN_SECONDS = 300;

    /** In-request memo so one request never probes the cooldown file twice. */
    private static $cooldown_cache = null;

    private static function cooldownFile(): string
    {
        return __DIR__ . '/../cache/ai_provider_cooldown.json';
    }

    private static function loadCooldowns(): array
    {
        if (self::$cooldown_cache !== null) {
            return self::$cooldown_cache;
        }
        $file = self::cooldownFile();
        $data = [];
        if (is_readable($file)) {
            $raw  = (string) @file_get_contents($file);
            $data = json_decode($raw, true) ?: [];
        }
        return self::$cooldown_cache = (is_array($data) ? $data : []);
    }

    /** True when $provider failed hard recently and should be skipped. */
    private static function isCoolingDown(string $provider): bool
    {
        $until = (int) (self::loadCooldowns()[$provider] ?? 0);
        return $until > time();
    }

    /**
     * Mark a provider as temporarily unusable. Only called for errors that will
     * still be true a second from now — quota, auth, and connectivity. Transient
     * 5xx and rate-limit-with-retry are deliberately NOT cooled down.
     */
    private static function markCooldown(string $provider, string $error): void
    {
        $hard = ['quota', 'exceeded your current', 'not configured', 'invalid api key',
                 'authentication', 'unauthorized', 'could not resolve host',
                 'timed out', 'ssl connection',
                 // Billing exhaustion — will still be true on the next request.
                 'credit balance', 'billing', 'insufficient',
                 // Daily caps, as opposed to per-minute ones, do not clear in seconds.
                 'tokens per day', 'requests per day', 'per day (tpd)'];
        $hit = false;
        foreach ($hard as $needle) {
            if (stripos($error, $needle) !== false) { $hit = true; break; }
        }
        if (!$hit) return;

        $data              = self::loadCooldowns();
        $data[$provider]   = time() + self::COOLDOWN_SECONDS;
        self::$cooldown_cache = $data;

        $file = self::cooldownFile();
        if (!is_dir(dirname($file))) {
            @mkdir(dirname($file), 0755, true);
        }
        @file_put_contents($file, json_encode($data), LOCK_EX);
    }

    /** Clear a provider's cooldown after a success. */
    private static function clearCooldown(string $provider): void
    {
        $data = self::loadCooldowns();
        if (!isset($data[$provider])) return;
        unset($data[$provider]);
        self::$cooldown_cache = $data;
        @file_put_contents(self::cooldownFile(), json_encode($data), LOCK_EX);
    }

    /**
     * Probe every provider with a trivial prompt and report status.
     * Diagnostics only — surfaced in the UI when AI_DEBUG is on.
     *
     * @return array<string, array{configured: bool, ok: bool, ms: int, error: ?string, cooling_down: bool}>
     */
    public static function healthCheck(): array
    {
        $out = [];
        foreach (self::$providers as $provider) {
            $row = [
                'configured'   => self::isConfigured($provider),
                'ok'           => false,
                'ms'           => 0,
                'error'        => null,
                'cooling_down' => self::isCoolingDown($provider),
            ];
            if ($row['configured']) {
                $t0 = microtime(true);
                try {
                    $r = self::callProvider($provider, 'Reply with the single word: OK', '', 20, false);
                    $row['ok']    = (bool) ($r['success'] ?? false);
                    $row['error'] = $row['ok'] ? null : (string) ($r['error'] ?? 'Unknown error');
                } catch (Throwable $e) {
                    $row['error'] = $e->getMessage();
                }
                $row['ms'] = (int) round((microtime(true) - $t0) * 1000);
            } else {
                $row['error'] = 'API key not configured';
            }
            $out[$provider] = $row;
        }
        return $out;
    }

    /**
     * Main method — try each provider in order until one succeeds.
     *
     * @param string $prompt      The full prompt to send
     * @param string $system      System/role instruction
     * @param int    $max_tokens  Max response tokens (default 1500)
     * @param bool   $json_mode   If true, instruct model to return JSON only
     * @return array {success, provider, content, error}
     */
    public static function generate(
        string $prompt,
        string $system = '',
        int $max_tokens = 1500,
        bool $json_mode = false
    ): array {
        $last_error = '';
        foreach (self::$providers as $provider) {
            if (self::isCoolingDown($provider)) {
                continue; // failed hard very recently — don't pay its timeout again
            }
            try {
                $result = self::callProvider($provider, $prompt, $system, $max_tokens, $json_mode);
                if ($result['success']) {
                    self::clearCooldown($provider);
                    self::logUsage($provider, $prompt, $result['content']);
                    return $result;
                }
                $last_error = $result['error'] ?? 'Unknown error';
                self::markCooldown($provider, $last_error);
                // Only log if the key was configured but the call still failed
                if (stripos($last_error, 'not configured') === false) {
                    error_log("RBCAIEngine [$provider] failed: $last_error");
                }
            } catch (Throwable $e) {
                $last_error = $e->getMessage();
                self::markCooldown($provider, $last_error);
                error_log("RBCAIEngine [$provider] exception: " . $e->getMessage());
            }
        }
        return [
            'success'  => false,
            'provider' => null,
            'content'  => null,
            'error'    => 'All AI providers failed. Last error: ' . $last_error,
        ];
    }

    private static function callProvider(
        string $provider,
        string $prompt,
        string $system,
        int $max_tokens,
        bool $json_mode
    ): array {
        switch ($provider) {
            case 'claude':        return self::callClaude($prompt, $system, $max_tokens, $json_mode);
            case 'claude_sonnet': return self::callClaude($prompt, $system, $max_tokens, $json_mode, 'claude-sonnet-5');
            case 'groq':          return self::callGroq($prompt, $system, $max_tokens, $json_mode);
            case 'gemini':        return self::callGemini($prompt, $system, $max_tokens, $json_mode);
            case 'openai':        return self::callOpenAI($prompt, $system, $max_tokens, $json_mode);
            default: return ['success' => false, 'error' => 'Unknown provider: ' . $provider];
        }
    }

    // ── CLAUDE ──────────────────────────────────────────────────────────────────
    private static function callClaude(
        string $prompt, string $system, int $max_tokens, bool $json_mode, ?string $model_override = null
    ): array {
        $api_key = '';
        if (defined('CLAUDE_API_KEY') && CLAUDE_API_KEY !== '' && CLAUDE_API_KEY !== 'YOUR_CLAUDE_API_KEY_HERE') {
            $api_key = CLAUDE_API_KEY;
        } else {
            $api_key = (string) getSettingValue('claude_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'Claude API key not configured'];
        }

        // $model_override lets the cascade run a specific Claude model (e.g.
        // the Sonnet last-resort fallback) without touching CLAUDE_MODEL.
        $model = $model_override ?: (defined('CLAUDE_MODEL') ? CLAUDE_MODEL : 'claude-haiku-4-5');
        $system_text = $system;
        if ($json_mode) {
            $system_text .= "\nRespond with valid JSON only. No markdown fences, no extra text.";
        }

        $body = [
            'model'      => $model,
            'max_tokens' => $max_tokens,
            'messages'   => [['role' => 'user', 'content' => $prompt]],
        ];
        if (!empty($system_text)) {
            $body['system'] = $system_text;
        }

        $ch = curl_init('https://api.anthropic.com/v1/messages');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'x-api-key: ' . $api_key,
                'anthropic-version: 2023-06-01',
            ],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'Claude cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        if ($http_code !== 200 || empty($data['content'][0]['text'])) {
            $msg = $data['error']['message'] ?? "Claude HTTP $http_code";
            return ['success' => false, 'error' => $msg];
        }
        return [
            'success'  => true,
            'provider' => 'claude',
            'content'  => $data['content'][0]['text'],
        ];
    }

    // ── GROQ ────────────────────────────────────────────────────────────────────
    private static function callGroq(
        string $prompt, string $system, int $max_tokens, bool $json_mode
    ): array {
        $api_key = '';
        if (defined('GROQ_API_KEY') && GROQ_API_KEY !== '' && GROQ_API_KEY !== 'YOUR_GROQ_API_KEY_HERE') {
            $api_key = GROQ_API_KEY;
        } else {
            $api_key = (string) getSettingValue('groq_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'Groq API key not configured'];
        }

        $model = defined('GROQ_MODEL') ? GROQ_MODEL : 'llama-3.3-70b-versatile';
        // Groq's free "on_demand" tier counts input + max_tokens toward a 12K
        // tokens-per-minute cap and 413s if exceeded. Cap output to leave room
        // for the prompt. Raise/remove this once on Groq Dev tier.
        $max_tokens = min($max_tokens, 6000);
        $messages = [];
        if (!empty($system)) {
            $messages[] = ['role' => 'system', 'content' => $system];
        }
        if ($json_mode) {
            $messages[] = ['role' => 'system', 'content' => 'Respond with valid JSON only. No markdown fences, no extra text.'];
        }
        $messages[] = ['role' => 'user', 'content' => $prompt];

        $body = [
            'model'       => $model,
            'messages'    => $messages,
            'max_tokens'  => $max_tokens,
            'temperature' => 0.7,
        ];
        if ($json_mode) {
            $body['response_format'] = ['type' => 'json_object'];
        }

        $ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $api_key,
            ],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'Groq cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        if ($http_code !== 200 || empty($data['choices'][0]['message']['content'])) {
            $msg = $data['error']['message'] ?? "Groq HTTP $http_code";
            return ['success' => false, 'error' => $msg];
        }
        return [
            'success'  => true,
            'provider' => 'groq',
            'content'  => $data['choices'][0]['message']['content'],
        ];
    }

    // ── GEMINI ──────────────────────────────────────────────────────────────────
    private static function callGemini(
        string $prompt, string $system, int $max_tokens, bool $json_mode
    ): array {
        $api_key = '';
        if (defined('GEMINI_API_KEY') && GEMINI_API_KEY !== '' && GEMINI_API_KEY !== 'YOUR_GEMINI_API_KEY_HERE') {
            $api_key = GEMINI_API_KEY;
        } else {
            $api_key = (string) getSettingValue('gemini_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'Gemini API key not configured'];
        }

        $model = defined('GEMINI_MODEL') ? GEMINI_MODEL : 'gemini-2.0-flash';
        // gemini-2.0-flash caps output at 8192 tokens; a larger request 400s.
        $max_tokens = min($max_tokens, 8192);

        $full_prompt = $prompt;
        if ($json_mode) {
            $full_prompt = "Respond with valid JSON only. No markdown fences.\n\n" . $full_prompt;
        }

        $payload = [
            'contents'         => [['role' => 'user', 'parts' => [['text' => $full_prompt]]]],
            'generationConfig' => ['maxOutputTokens' => $max_tokens, 'temperature' => 0.7],
        ];
        if (!empty($system)) {
            $payload['system_instruction'] = ['parts' => [['text' => $system]]];
        }

        $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'Gemini cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
        if ($http_code !== 200 || empty($text)) {
            $msg = $data['error']['message'] ?? "Gemini HTTP $http_code";
            return ['success' => false, 'error' => $msg];
        }
        return [
            'success'  => true,
            'provider' => 'gemini',
            'content'  => $text,
        ];
    }

    // ── OPENAI ──────────────────────────────────────────────────────────────────
    private static function callOpenAI(
        string $prompt, string $system, int $max_tokens, bool $json_mode
    ): array {
        // Check multiple possible key locations
        $api_key = '';
        if (defined('OPENAI_API_KEY') && OPENAI_API_KEY !== '') {
            $api_key = OPENAI_API_KEY;
        } elseif (defined('AI_OPENAI_API_KEY') && AI_OPENAI_API_KEY !== '') {
            $api_key = AI_OPENAI_API_KEY;
        } else {
            $api_key = (string) getSettingValue('openai_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'OpenAI API key not configured'];
        }

        $messages = [];
        if (!empty($system)) {
            $messages[] = ['role' => 'system', 'content' => $system];
        }
        $messages[] = ['role' => 'user', 'content' => $prompt];

        $body = [
            'model'      => 'gpt-4o-mini',
            'messages'   => $messages,
            'max_tokens' => $max_tokens,
        ];
        if ($json_mode) {
            $body['response_format'] = ['type' => 'json_object'];
        }

        $ch = curl_init('https://api.openai.com/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $api_key,
            ],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'OpenAI cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        if ($http_code !== 200 || empty($data['choices'][0]['message']['content'])) {
            $msg = $data['error']['message'] ?? "OpenAI HTTP $http_code";
            return ['success' => false, 'error' => $msg];
        }
        return [
            'success'  => true,
            'provider' => 'openai',
            'content'  => $data['choices'][0]['message']['content'],
        ];
    }

    // ── USAGE LOGGING ────────────────────────────────────────────────────────────
    /**
     * Record a successful call for reporting.
     *
     * Best-effort ONLY. This runs *after* the AI has already answered, so a
     * logging failure must never be allowed to propagate — mysqli throws by
     * default in PHP 8, and an uncaught throw here is caught by the cascade's
     * error handler, which then discards a perfectly good response and falls
     * through to the next provider. Swallow everything.
     */
    private static function logUsage(string $provider, string $prompt, string $response): void
    {
        try {
            $conn = getDBConnection();
            if (!$conn) return;
            $user_id        = $_SESSION['user_id'] ?? null;
            $prompt_short   = substr($prompt, 0, 200);
            $response_short = substr($response, 0, 200);
            $stmt = $conn->prepare(
                "INSERT INTO ai_usage_log
                 (provider, prompt_preview, response_preview, user_id, created_at)
                 VALUES (?,?,?,?,NOW())"
            );
            if ($stmt) {
                $stmt->bind_param('sssi', $provider, $prompt_short, $response_short, $user_id);
                $stmt->execute();
                $stmt->close();
            }
        } catch (Throwable $e) {
            error_log('RBCAIEngine::logUsage failed (non-fatal): ' . $e->getMessage());
        }
    }

    // ── MULTI-TURN / HISTORY SUPPORT ────────────────────────────────────────

    /**
     * Send a full conversation history array to the AI.
     * Each element: ['role' => 'user'|'assistant', 'content' => '...']
     *
     * Falls back through Claude → Groq → Gemini → OpenAI.
     */
    public static function generateWithHistory(
        array  $messages,
        string $system     = '',
        int    $max_tokens = 2000,
        bool   $json_mode  = false
    ): array {
        $last_error = '';
        foreach (self::$providers as $provider) {
            if (self::isCoolingDown($provider)) {
                continue; // failed hard very recently — don't pay its timeout again
            }
            try {
                $result = self::callProviderWithHistory(
                    $provider, $messages, $system, $max_tokens, $json_mode
                );
                if ($result['success']) {
                    self::clearCooldown($provider);
                    self::logUsage(
                        $result['provider'],
                        // log a preview of the last user turn
                        (string) ($messages[count($messages) - 1]['content'] ?? ''),
                        (string) ($result['content'] ?? '')
                    );
                    return $result;
                }
                $last_error = $result['error'] ?? 'Unknown error';
                self::markCooldown($provider, $last_error);
                if (stripos($last_error, 'not configured') === false) {
                    error_log("RBCAIEngine [{$provider}] (history) failed: {$last_error}");
                }
            } catch (Throwable $e) {
                $last_error = $e->getMessage();
                self::markCooldown($provider, $last_error);
                error_log("RBCAIEngine [{$provider}] (history) exception: " . $e->getMessage());
            }
        }
        return [
            'success'  => false,
            'provider' => null,
            'content'  => null,
            'error'    => 'All AI providers failed. Last error: ' . $last_error,
        ];
    }

    private static function callProviderWithHistory(
        string $provider,
        array  $messages,
        string $system,
        int    $max_tokens,
        bool   $json_mode
    ): array {
        switch ($provider) {
            case 'claude':
                return self::callClaudeWithHistory($messages, $system, $max_tokens, $json_mode);
            case 'claude_sonnet':
                return self::callClaudeWithHistory($messages, $system, $max_tokens, $json_mode, 'claude-sonnet-5');
            case 'groq':
                return self::callGroqWithHistory($messages, $system, $max_tokens, $json_mode);
            case 'gemini':
                return self::callGeminiWithHistory($messages, $system, $max_tokens, $json_mode);
            case 'openai':
                return self::callOpenAIWithHistory($messages, $system, $max_tokens, $json_mode);
            default:
                return ['success' => false, 'error' => 'Unknown provider: ' . $provider];
        }
    }

    private static function callClaudeWithHistory(
        array $messages, string $system, int $max_tokens, bool $json_mode, ?string $model_override = null
    ): array {
        $api_key = '';
        if (defined('CLAUDE_API_KEY') && CLAUDE_API_KEY !== '' && CLAUDE_API_KEY !== 'YOUR_CLAUDE_API_KEY_HERE') {
            $api_key = CLAUDE_API_KEY;
        } else {
            $api_key = (string) getSettingValue('claude_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'Claude API key not configured'];
        }

        $model       = $model_override ?: (defined('CLAUDE_MODEL') ? CLAUDE_MODEL : 'claude-haiku-4-5');
        $system_text = $system;
        if ($json_mode) {
            $system_text .= "\nRespond with valid JSON only. No markdown fences, no extra text.";
        }

        $body = [
            'model'      => $model,
            'max_tokens' => $max_tokens,
            'messages'   => $messages,  // Claude natively handles multi-turn arrays
        ];
        if (!empty($system_text)) {
            $body['system'] = $system_text;
        }

        $ch = curl_init('https://api.anthropic.com/v1/messages');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'x-api-key: ' . $api_key,
                'anthropic-version: 2023-06-01',
            ],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response  = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'Claude cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        if ($http_code !== 200 || empty($data['content'][0]['text'])) {
            $msg = $data['error']['message'] ?? "Claude HTTP {$http_code}";
            return ['success' => false, 'error' => $msg];
        }
        return ['success' => true, 'provider' => 'claude', 'content' => $data['content'][0]['text']];
    }

    private static function callGroqWithHistory(
        array $messages, string $system, int $max_tokens, bool $json_mode
    ): array {
        $api_key = '';
        if (defined('GROQ_API_KEY') && GROQ_API_KEY !== '' && GROQ_API_KEY !== 'YOUR_GROQ_API_KEY_HERE') {
            $api_key = GROQ_API_KEY;
        } else {
            $api_key = (string) getSettingValue('groq_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'Groq API key not configured'];
        }

        $model    = defined('GROQ_MODEL') ? GROQ_MODEL : 'llama-3.3-70b-versatile';
        $full     = [];
        if ($system)    $full[] = ['role' => 'system', 'content' => $system];
        if ($json_mode) $full[] = ['role' => 'system', 'content' => 'Respond with valid JSON only. No markdown fences.'];
        $full = array_merge($full, $messages);

        $body = [
            'model'       => $model,
            'messages'    => $full,
            'max_tokens'  => $max_tokens,
            'temperature' => 0.7,
        ];
        if ($json_mode) $body['response_format'] = ['type' => 'json_object'];

        $ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $api_key,
            ],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response  = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'Groq cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        if ($http_code !== 200 || empty($data['choices'][0]['message']['content'])) {
            $msg = $data['error']['message'] ?? "Groq HTTP {$http_code}";
            return ['success' => false, 'error' => $msg];
        }
        return ['success' => true, 'provider' => 'groq', 'content' => $data['choices'][0]['message']['content']];
    }

    private static function callGeminiWithHistory(
        array $messages, string $system, int $max_tokens, bool $json_mode
    ): array {
        $api_key = '';
        if (defined('GEMINI_API_KEY') && GEMINI_API_KEY !== '' && GEMINI_API_KEY !== 'YOUR_GEMINI_API_KEY_HERE') {
            $api_key = GEMINI_API_KEY;
        } else {
            $api_key = (string) getSettingValue('gemini_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'Gemini API key not configured'];
        }

        $model = defined('GEMINI_MODEL') ? GEMINI_MODEL : 'gemini-2.0-flash';
        // gemini-2.0-flash caps output at 8192 tokens; a larger request 400s.
        $max_tokens = min($max_tokens, 8192);

        // Flatten history into a single prompt for Gemini
        $flat = $json_mode ? "Respond with valid JSON only. No markdown fences.\n\n" : '';
        foreach ($messages as $msg) {
            $flat .= strtoupper((string) ($msg['role'] ?? 'user')) . ': '
                   . (string) ($msg['content'] ?? '') . "\n";
        }

        $payload = [
            'contents'         => [['role' => 'user', 'parts' => [['text' => $flat]]]],
            'generationConfig' => ['maxOutputTokens' => $max_tokens, 'temperature' => 0.7],
        ];
        if (!empty($system)) {
            $payload['system_instruction'] = ['parts' => [['text' => $system]]];
        }

        $url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$api_key}";
        $ch  = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response  = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'Gemini cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? null;
        if ($http_code !== 200 || empty($text)) {
            $msg = $data['error']['message'] ?? "Gemini HTTP {$http_code}";
            return ['success' => false, 'error' => $msg];
        }
        return ['success' => true, 'provider' => 'gemini', 'content' => $text];
    }

    private static function callOpenAIWithHistory(
        array $messages, string $system, int $max_tokens, bool $json_mode
    ): array {
        $api_key = '';
        if (defined('OPENAI_API_KEY') && OPENAI_API_KEY !== '') {
            $api_key = OPENAI_API_KEY;
        } elseif (defined('AI_OPENAI_API_KEY') && AI_OPENAI_API_KEY !== '') {
            $api_key = AI_OPENAI_API_KEY;
        } else {
            $api_key = (string) getSettingValue('openai_api_key');
        }
        if (empty($api_key)) {
            return ['success' => false, 'error' => 'OpenAI API key not configured'];
        }

        $full = [];
        if ($system) $full[] = ['role' => 'system', 'content' => $system];
        $full = array_merge($full, $messages);

        $body = [
            'model'      => 'gpt-4o-mini',
            'messages'   => $full,
            'max_tokens' => $max_tokens,
        ];
        if ($json_mode) $body['response_format'] = ['type' => 'json_object'];

        $ch = curl_init('https://api.openai.com/v1/chat/completions');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode($body),
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $api_key,
            ],
            CURLOPT_TIMEOUT        => self::HTTP_TIMEOUT,
            CURLOPT_CONNECTTIMEOUT => self::HTTP_CONNECT_TIMEOUT,
        ]);
        $response  = curl_exec($ch);
        $http_code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curl_err  = curl_error($ch);
        curl_close($ch);

        if ($curl_err) {
            return ['success' => false, 'error' => 'OpenAI cURL error: ' . $curl_err];
        }
        $data = json_decode((string) $response, true);
        if ($http_code !== 200 || empty($data['choices'][0]['message']['content'])) {
            $msg = $data['error']['message'] ?? "OpenAI HTTP {$http_code}";
            return ['success' => false, 'error' => $msg];
        }
        return ['success' => true, 'provider' => 'openai', 'content' => $data['choices'][0]['message']['content']];
    }

    /**
     * Check whether an API key is configured for a given provider.
     */
    public static function isConfigured(string $provider): bool
    {
        switch ($provider) {
            case 'claude':
            case 'claude_sonnet':
                $k = defined('CLAUDE_API_KEY') ? CLAUDE_API_KEY : '';
                if ($k && $k !== 'YOUR_CLAUDE_API_KEY_HERE') return true;
                return !empty(getSettingValue('claude_api_key'));
            case 'groq':
                $k = defined('GROQ_API_KEY') ? GROQ_API_KEY : '';
                if ($k && $k !== 'YOUR_GROQ_API_KEY_HERE') return true;
                return !empty(getSettingValue('groq_api_key'));
            case 'gemini':
                $k = defined('GEMINI_API_KEY') ? GEMINI_API_KEY : '';
                if ($k && $k !== 'YOUR_GEMINI_API_KEY_HERE') return true;
                return !empty(getSettingValue('gemini_api_key'));
            case 'openai':
                if (defined('OPENAI_API_KEY') && OPENAI_API_KEY !== '') return true;
                if (defined('AI_OPENAI_API_KEY') && AI_OPENAI_API_KEY !== '') return true;
                return !empty(getSettingValue('openai_api_key'));
            default:
                return false;
        }
    }
}
