<?php
declare(strict_types=1);

require_once dirname(__DIR__, 2) . '/config.php';

/**
 * AI Engine Abstraction Layer — EduTrack Exams Generator
 *
 * Priority:
 *   1. Claude API  (primary)
 *   2. Groq API    (first fallback)
 *   3. Gemini API  (last fallback)
 *
 * Always call callAI() — never call engine functions directly
 * in application code.
 */

function callClaude(string $system, string $user, int $maxTokens = 4096): string
{
    if (!defined('CLAUDE_API_KEY') || empty(CLAUDE_API_KEY) || CLAUDE_API_KEY === 'YOUR_CLAUDE_API_KEY_HERE') {
        throw new Exception('Claude API key not configured');
    }

    $payload = [
        'model' => CLAUDE_MODEL,
        'max_tokens' => $maxTokens,
        'system' => $system,
        'messages' => [['role' => 'user', 'content' => $user]],
    ];

    $ch = curl_init('https://api.anthropic.com/v1/messages');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 120,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'x-api-key: ' . CLAUDE_API_KEY,
            'anthropic-version: 2023-06-01',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

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

    if ($curlErr) {
        throw new Exception("Claude cURL error: $curlErr");
    }
    if ($httpCode === 429) {
        throw new Exception('Claude rate limit reached (429)');
    }
    if ($httpCode === 529) {
        throw new Exception('Claude overloaded (529)');
    }
    if ($httpCode !== 200) {
        $body = json_decode((string) $response, true);
        $msg = $body['error']['message'] ?? substr((string) $response, 0, 200);
        throw new Exception("Claude API HTTP $httpCode: $msg");
    }

    $data = json_decode((string) $response, true);
    $text = $data['content'][0]['text'] ?? '';

    if (empty(trim((string) $text))) {
        throw new Exception('Claude returned empty response');
    }

    return (string) $text;
}

function callGroq(string $system, string $user, int $maxTokens = 4096): string
{
    if (!defined('GROQ_API_KEY') || empty(GROQ_API_KEY) || GROQ_API_KEY === 'YOUR_GROQ_API_KEY_HERE') {
        throw new Exception('Groq API key not configured');
    }

    $payload = [
        'model' => GROQ_MODEL,
        'max_tokens' => $maxTokens,
        'temperature' => 0.7,
        'messages' => [
            ['role' => 'system', 'content' => $system],
            ['role' => 'user', 'content' => $user],
        ],
    ];

    $ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . GROQ_API_KEY,
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

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

    if ($curlErr) {
        throw new Exception("Groq cURL error: $curlErr");
    }
    if ($httpCode === 429) {
        throw new Exception('Groq rate limit reached (429)');
    }
    if ($httpCode !== 200) {
        $body = json_decode((string) $response, true);
        $msg = $body['error']['message'] ?? substr((string) $response, 0, 200);
        throw new Exception("Groq API HTTP $httpCode: $msg");
    }

    $data = json_decode((string) $response, true);
    $text = $data['choices'][0]['message']['content'] ?? '';

    if (empty(trim((string) $text))) {
        throw new Exception('Groq returned empty response');
    }

    return (string) $text;
}

function callGemini(string $system, string $user, int $maxTokens = 4096): string
{
    if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY) || GEMINI_API_KEY === 'YOUR_GEMINI_API_KEY_HERE') {
        throw new Exception('Gemini API key not configured');
    }

    // gemini-2.0-flash caps output at 8192 tokens; a larger request 400s.
    $maxTokens = min($maxTokens, 8192);

    $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . GEMINI_MODEL . ':generateContent?key=' . GEMINI_API_KEY;
    $payload = [
        'system_instruction' => ['parts' => [['text' => $system]]],
        'contents' => [[
            'role' => 'user',
            'parts' => [['text' => $user]],
        ]],
        'generationConfig' => [
            'maxOutputTokens' => $maxTokens,
            'temperature' => 0.7,
        ],
    ];

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

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

    if ($curlErr) {
        throw new Exception("Gemini cURL error: $curlErr");
    }
    if ($httpCode === 429) {
        throw new Exception('Gemini rate limit reached (429)');
    }
    if ($httpCode !== 200) {
        $body = json_decode((string) $response, true);
        $msg = $body['error']['message'] ?? substr((string) $response, 0, 200);
        throw new Exception("Gemini API HTTP $httpCode: $msg");
    }

    $data = json_decode((string) $response, true);
    $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';

    if (empty(trim((string) $text))) {
        $reason = $data['candidates'][0]['finishReason'] ?? 'UNKNOWN';
        throw new Exception("Gemini empty response — finishReason: $reason");
    }

    return (string) $text;
}

function getEnginePriority(mysqli $conn): array
{
    // Cheap-first, matching RBCAIEngine. Claude (Haiku) last of the three so a
    // slow/unreachable Claude doesn't add another 120s hang on the fallback path.
    $default = ['gemini', 'groq', 'claude'];
    $result = @$conn->query("SELECT setting_value FROM ai_engine_config WHERE setting_key='engine_priority' LIMIT 1");
    $row = $result ? $result->fetch_assoc() : null;
    $order = $row && !empty($row['setting_value']) ? explode(',', (string) $row['setting_value']) : $default;
    $order = array_values(array_intersect(array_map('trim', $order), $default));
    return $order !== [] ? $order : $default;
}

/**
 * @return array{result:?string,engine:?string,error:?string,attempts:array<int,array{engine:string,error:string}>}
 */
function callAI(string $system, string $user, int $maxTokens = 4096): array
{
    $priority = ['claude', 'groq', 'gemini'];
    $conn = getDBConnection();
    if ($conn instanceof mysqli) {
        $priority = getEnginePriority($conn);
    }

    $all = [
        'claude' => fn() => callClaude($system, $user, $maxTokens),
        'groq' => fn() => callGroq($system, $user, $maxTokens),
        'gemini' => fn() => callGemini($system, $user, $maxTokens),
    ];

    $attempts = [];
    foreach ($priority as $name) {
        if (!isset($all[$name])) {
            continue;
        }
        try {
            $result = $all[$name]();
            if (!empty(trim($result))) {
                error_log("[EduTrack AI] Success — engine: $name");
                return [
                    'result' => $result,
                    'engine' => $name,
                    'error' => null,
                    'attempts' => $attempts,
                ];
            }
            throw new Exception('Empty result returned');
        } catch (Exception $e) {
            $attempts[] = ['engine' => $name, 'error' => $e->getMessage()];
            error_log("[EduTrack AI] $name failed: " . $e->getMessage() . ' — trying next engine');
        }
    }

    $summary = implode(' → ', array_map(static fn(array $a): string => $a['engine'] . ': ' . $a['error'], $attempts));
    error_log("[EduTrack AI] All engines failed: $summary");

    return [
        'result' => null,
        'engine' => null,
        'error' => 'All AI engines are currently unavailable. Please try again in a few minutes.',
        'attempts' => $attempts,
    ];
}

function callClaudeWithImage(string $system, string $base64Image, string $mediaType, int $maxTokens = 4096): string
{
    if (!defined('CLAUDE_API_KEY') || empty(CLAUDE_API_KEY) || CLAUDE_API_KEY === 'YOUR_CLAUDE_API_KEY_HERE') {
        throw new Exception('Claude API key not configured for image extraction');
    }

    $payload = [
        'model' => CLAUDE_MODEL,
        'max_tokens' => $maxTokens,
        'system' => $system,
        'messages' => [[
            'role' => 'user',
            'content' => [
                [
                    'type' => 'image',
                    'source' => [
                        'type' => 'base64',
                        'media_type' => $mediaType,
                        'data' => $base64Image,
                    ],
                ],
                [
                    'type' => 'text',
                    'text' => 'Extract ALL educational content from this study material: topics, definitions, formulas, concepts, examples, and any diagrams described. Be thorough and structured.',
                ],
            ],
        ]],
    ];

    $ch = curl_init('https://api.anthropic.com/v1/messages');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 120,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'x-api-key: ' . CLAUDE_API_KEY,
            'anthropic-version: 2023-06-01',
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

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

    if ($curlErr) {
        throw new Exception("Claude Vision cURL error: $curlErr");
    }
    if ($httpCode !== 200) {
        throw new Exception('Claude Vision HTTP ' . $httpCode . ': ' . substr((string) $response, 0, 200));
    }

    $data = json_decode((string) $response, true);
    return (string) ($data['content'][0]['text'] ?? '');
}

/**
 * Extract text from an image using Gemini Vision (gemini-2.0-flash supports multimodal).
 * Fallback option when Claude key is absent or fails.
 */
function callGeminiWithImage(string $system, string $base64Image, string $mediaType, int $maxTokens = 4096): string
{
    if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY) || GEMINI_API_KEY === 'YOUR_GEMINI_API_KEY_HERE') {
        throw new Exception('Gemini API key not configured for image extraction');
    }

    // gemini-2.0-flash supports inline image parts
    $model = defined('GEMINI_MODEL') ? GEMINI_MODEL : 'gemini-2.0-flash';
    $url   = 'https://generativelanguage.googleapis.com/v1beta/models/' . $model . ':generateContent?key=' . GEMINI_API_KEY;

    $payload = [
        'system_instruction' => ['parts' => [['text' => $system]]],
        'contents' => [[
            'role'  => 'user',
            'parts' => [
                ['inline_data' => ['mime_type' => $mediaType, 'data' => $base64Image]],
                ['text'        => 'Extract ALL educational content from this study material: topics, definitions, formulas, concepts, examples, and any diagrams described. Be thorough and structured.'],
            ],
        ]],
        'generationConfig' => ['maxOutputTokens' => $maxTokens, 'temperature' => 0.3],
    ];

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS     => json_encode($payload),
    ]);

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

    if ($curlErr) {
        throw new Exception("Gemini Vision cURL error: $curlErr");
    }
    if ($httpCode !== 200) {
        $body = json_decode((string) $response, true);
        $msg  = $body['error']['message'] ?? substr((string) $response, 0, 200);
        throw new Exception("Gemini Vision HTTP $httpCode: $msg");
    }

    $data = json_decode((string) $response, true);
    $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';

    if (empty(trim((string) $text))) {
        $reason = $data['candidates'][0]['finishReason'] ?? 'UNKNOWN';
        throw new Exception("Gemini Vision empty response — finishReason: $reason");
    }

    return (string) $text;
}

/**
 * Extract text from an image using Groq Vision.
 * Uses meta-llama/llama-4-scout-17b-16e-instruct (multimodal) via OpenAI-compatible API.
 * Fallback option when Claude and Gemini are unavailable.
 */
function callGroqWithImage(string $system, string $base64Image, string $mediaType, int $maxTokens = 4096): string
{
    if (!defined('GROQ_API_KEY') || empty(GROQ_API_KEY) || GROQ_API_KEY === 'YOUR_GROQ_API_KEY_HERE') {
        throw new Exception('Groq API key not configured for image extraction');
    }

    // Use a multimodal-capable Groq model (Llama 4 Scout supports vision)
    $visionModel = 'meta-llama/llama-4-scout-17b-16e-instruct';
    $dataUrl     = 'data:' . $mediaType . ';base64,' . $base64Image;

    $payload = [
        'model'      => $visionModel,
        'max_tokens' => $maxTokens,
        'messages'   => [
            ['role' => 'system', 'content' => $system],
            [
                'role'    => 'user',
                'content' => [
                    ['type' => 'image_url', 'image_url' => ['url' => $dataUrl]],
                    ['type' => 'text',      'text'      => 'Extract ALL educational content from this study material: topics, definitions, formulas, concepts, examples, and any diagrams described. Be thorough and structured.'],
                ],
            ],
        ],
    ];

    $ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_TIMEOUT        => 120,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . GROQ_API_KEY,
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

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

    if ($curlErr) {
        throw new Exception("Groq Vision cURL error: $curlErr");
    }
    if ($httpCode !== 200) {
        $body = json_decode((string) $response, true);
        $msg  = $body['error']['message'] ?? substr((string) $response, 0, 200);
        throw new Exception("Groq Vision HTTP $httpCode: $msg");
    }

    $data = json_decode((string) $response, true);
    $text = $data['choices'][0]['message']['content'] ?? '';

    if (empty(trim((string) $text))) {
        throw new Exception('Groq Vision returned empty response');
    }

    return (string) $text;
}

/**
 * Vision cascade: Claude → Gemini → Groq.
 * Tries each configured provider in order and returns the first successful extraction.
 */
function callVisionCascade(string $base64Image, string $mediaType): string
{
    $system = 'You are an educational content extractor.';

    $engines = [
        'Claude'  => fn() => callClaudeWithImage($system, $base64Image, $mediaType),
        'Gemini'  => fn() => callGeminiWithImage($system, $base64Image, $mediaType),
        'Groq'    => fn() => callGroqWithImage($system, $base64Image, $mediaType),
    ];

    $errors = [];
    foreach ($engines as $name => $fn) {
        try {
            $result = $fn();
            if (!empty(trim($result))) {
                error_log("[EduTrack Vision] Success — engine: $name");
                return $result;
            }
            throw new Exception('Empty result');
        } catch (Exception $e) {
            $errors[] = "$name: " . $e->getMessage();
            error_log("[EduTrack Vision] $name failed: " . $e->getMessage());
        }
    }

    throw new Exception('All vision engines failed — ' . implode(' | ', $errors));
}

/**
 * True when the raw text (after stripping markdown fences) decodes as JSON with
 * no repair needed. A false result means the payload was malformed or — most
 * commonly — truncated at the token limit, which is the signal callers use to
 * decide whether to re-run a generation in smaller batches.
 */
function jsonDecodesCleanly(string $raw): bool
{
    $raw = preg_replace('/^```json\s*/i', '', trim($raw));
    $raw = preg_replace('/^```\s*/i', '', (string) $raw);
    $raw = preg_replace('/\s*```$/', '', (string) $raw);
    return is_array(json_decode(trim((string) $raw), true));
}

function repairAndParseJSON(string $raw): ?array
{
    $raw = preg_replace('/^```json\s*/i', '', trim($raw));
    $raw = preg_replace('/^```\s*/i', '', (string) $raw);
    $raw = preg_replace('/\s*```$/', '', (string) $raw);
    $raw = trim((string) $raw);

    // 1. Happy path — the whole thing is already valid JSON.
    $result = json_decode($raw, true);
    if (is_array($result)) {
        return $result;
    }

    // 2. Extract the outermost [ ... ] array and try again (strips stray prose).
    $start = strpos($raw, '[');
    $end = strrpos($raw, ']');
    if ($start !== false && $end !== false && $end > $start) {
        $extracted = substr($raw, $start, $end - $start + 1);
        $result = json_decode($extracted, true);
        if (is_array($result)) {
            return $result;
        }
    }

    // 3. Salvage complete top-level objects from a malformed/truncated array.
    //    When a generation is cut off at max_tokens the array never closes, so
    //    steps 1-2 fail and every already-written question would be lost. Here
    //    we keep each fully-written { ... } object and discard only the final
    //    partial one.
    $arrStart = strpos($raw, '[');
    if ($arrStart !== false) {
        $objects = extractCompleteJsonObjects(substr($raw, $arrStart));
        if ($objects !== []) {
            error_log('[EduTrack AI] Recovered ' . count($objects)
                . ' complete object(s) from a malformed/truncated array.');
            return $objects;
        }
    }

    // 4. Single object → wrap as a one-element array.
    $start = strpos($raw, '{');
    $end = strrpos($raw, '}');
    if ($start !== false && $end !== false && $end > $start) {
        $extracted = substr($raw, $start, $end - $start + 1);
        $result = json_decode($extracted, true);
        if (is_array($result)) {
            return [$result];
        }
    }

    error_log('[EduTrack AI] JSON repair failed. Raw: ' . substr($raw, 0, 500));
    return null;
}

/**
 * Scan a string for top-level {...} objects and return each one that parses as
 * valid JSON. Brace counting is string- and escape-aware, so braces or brackets
 * appearing inside string values never confuse the object boundaries. A trailing
 * object truncated mid-write (unbalanced braces / unterminated string) is simply
 * skipped, which is what lets us rescue a response cut off at max_tokens.
 *
 * @return array<int,array<string,mixed>>
 */
function extractCompleteJsonObjects(string $raw): array
{
    $objects  = [];
    $depth    = 0;
    $inString = false;
    $escaped  = false;
    $objStart = -1;
    $len      = strlen($raw);

    for ($i = 0; $i < $len; $i++) {
        $ch = $raw[$i];

        if ($inString) {
            if ($escaped) {
                $escaped = false;
            } elseif ($ch === '\\') {
                $escaped = true;
            } elseif ($ch === '"') {
                $inString = false;
            }
            continue;
        }

        if ($ch === '"') {
            $inString = true;
        } elseif ($ch === '{') {
            if ($depth === 0) {
                $objStart = $i;
            }
            $depth++;
        } elseif ($ch === '}') {
            if ($depth > 0) {
                $depth--;
                if ($depth === 0 && $objStart >= 0) {
                    $decoded = json_decode(substr($raw, $objStart, $i - $objStart + 1), true);
                    if (is_array($decoded)) {
                        $objects[] = $decoded;
                    }
                    $objStart = -1;
                }
            }
        }
    }

    return $objects;
}
