<?php

declare(strict_types=1);

namespace PpPhp\Core\Ai;

/**
 * Minimal, dependency-free multi-provider AI client.
 *
 * Bring your own provider + key — Anthropic (Claude), OpenAI (ChatGPT), Google
 * Gemini, DeepSeek, Groq, Mistral, OpenRouter, or any OpenAI-compatible endpoint
 * (Custom + base URL). Used by the platform AND shipped into generated apps
 * (copyCore). Config comes from `config/ai.php` (written from Misc → AI settings)
 * or a provider env var — the key is never hard-coded or sent to the browser.
 *
 *   $r = AiClient::complete('You are a Nepali legal assistant.', 'Summarise…');
 *   if ($r['success']) echo $r['text'];
 */
final class AiClient
{
    private const DEFAULT_MODEL = 'claude-sonnet-5';
    private const ANTHROPIC_VERSION = '2023-06-01';

    private static ?array $config = null;

    /**
     * Provider registry. `kind` selects the request/response shape:
     *   anthropic — Messages API      openai — Chat Completions      gemini — generateContent
     * `base` is the API root (overridable per-config for Custom); `model` is the
     * sensible default when none is set; `key_env` is the fallback env var.
     *
     * @return array<string, array{label:string, kind:string, base:string, model:string, key_env:string}>
     */
    public static function providers(): array
    {
        return [
            'anthropic'  => ['label' => 'Anthropic (Claude)',            'kind' => 'anthropic', 'base' => 'https://api.anthropic.com/v1', 'model' => 'claude-sonnet-5',            'key_env' => 'ANTHROPIC_API_KEY'],
            'openai'     => ['label' => 'OpenAI (ChatGPT)',              'kind' => 'openai',    'base' => 'https://api.openai.com/v1',    'model' => 'gpt-4o-mini',              'key_env' => 'OPENAI_API_KEY'],
            'gemini'     => ['label' => 'Google Gemini',                 'kind' => 'gemini',    'base' => 'https://generativelanguage.googleapis.com/v1beta', 'model' => 'gemini-2.0-flash', 'key_env' => 'GEMINI_API_KEY'],
            'deepseek'   => ['label' => 'DeepSeek',                      'kind' => 'openai',    'base' => 'https://api.deepseek.com',     'model' => 'deepseek-chat',            'key_env' => 'DEEPSEEK_API_KEY'],
            'groq'       => ['label' => 'Groq (Llama/Mixtral)',          'kind' => 'openai',    'base' => 'https://api.groq.com/openai/v1', 'model' => 'llama-3.3-70b-versatile', 'key_env' => 'GROQ_API_KEY'],
            'mistral'    => ['label' => 'Mistral',                       'kind' => 'openai',    'base' => 'https://api.mistral.ai/v1',    'model' => 'mistral-small-latest',     'key_env' => 'MISTRAL_API_KEY'],
            'openrouter' => ['label' => 'OpenRouter (many models)',      'kind' => 'openai',    'base' => 'https://openrouter.ai/api/v1', 'model' => 'openai/gpt-4o-mini',       'key_env' => 'OPENROUTER_API_KEY'],
            'custom'     => ['label' => 'Custom (OpenAI-compatible)',    'kind' => 'openai',    'base' => '',                             'model' => '',                         'key_env' => 'AI_API_KEY'],
        ];
    }

    /** Explicitly set config (tests / platform). Otherwise auto-loaded. */
    public static function setConfig(array $config): void
    {
        self::$config = $config;
    }

    /** Resolve config from an injected array, config/ai.php, or a provider env var. */
    private static function config(): array
    {
        if (self::$config !== null) {
            return self::$config;
        }
        $cfg = [];
        foreach ([\defined('BASE_PATH') ? \constant('BASE_PATH') . '/config/ai.php' : null] as $f) {
            if ($f !== null && is_file($f)) {
                $loaded = require $f;
                if (is_array($loaded)) $cfg = $loaded;
                break;
            }
        }
        if (trim((string) ($cfg['api_key'] ?? '')) === '') {
            $meta = self::meta((string) ($cfg['provider'] ?? 'anthropic'));
            $env = getenv($meta['key_env']) ?: getenv('ANTHROPIC_API_KEY');
            if (is_string($env) && $env !== '') $cfg['api_key'] = $env;
        }
        return self::$config = $cfg;
    }

    /** Provider metadata with an Anthropic fallback for unknown ids. */
    private static function meta(string $provider): array
    {
        $all = self::providers();
        return $all[$provider] ?? $all['anthropic'];
    }

    public static function isConfigured(): bool
    {
        return trim((string) (self::config()['api_key'] ?? '')) !== '';
    }

    /**
     * One-shot completion, routed to the configured provider.
     *
     * @param array $opts model, max_tokens, temperature, and optionally provider,
     *                    api_key, base_url (to override config, e.g. for testing).
     * @return array{success:bool, text:string, error:string, model:string}
     */
    public static function complete(string $system, string $user, array $opts = []): array
    {
        $cfg = self::config();
        $provider = (string) ($opts['provider'] ?? $cfg['provider'] ?? 'anthropic');
        $meta = self::meta($provider);
        $key = trim((string) ($opts['api_key'] ?? $cfg['api_key'] ?? ''));
        if ($key === '') {
            return ['success' => false, 'text' => '', 'error' => 'AI is not configured. Add a provider API key in Settings.', 'model' => ''];
        }
        $model = (string) ($opts['model'] ?? $cfg['model'] ?? '') ?: ($meta['model'] ?: self::DEFAULT_MODEL);
        $maxTokens = (int) ($opts['max_tokens'] ?? $cfg['max_tokens'] ?? 1024);
        $base = rtrim(trim((string) ($opts['base_url'] ?? $cfg['base_url'] ?? '')) ?: $meta['base'], '/');
        $temp = isset($opts['temperature']) ? (float) $opts['temperature'] : null;

        if ($base === '') {
            return ['success' => false, 'text' => '', 'error' => 'This provider needs a Base URL (Custom / OpenAI-compatible).', 'model' => $model];
        }

        [$url, $headers, $payload] = self::buildRequest($meta['kind'], $base, $key, $model, $maxTokens, $system, $user, $temp);
        [$body, $code, $err] = self::httpPost($url, $headers, $payload);
        if ($err !== '') {
            return ['success' => false, 'text' => '', 'error' => 'Network error: ' . $err, 'model' => $model];
        }
        $json = json_decode((string) $body, true);
        if ($code < 200 || $code >= 300 || !is_array($json)) {
            return ['success' => false, 'text' => '', 'error' => self::errorMessage($json, $code), 'model' => $model];
        }
        return ['success' => true, 'text' => self::extractText($meta['kind'], $json), 'error' => '', 'model' => $model];
    }

    /**
     * Validate a provider + key with a tiny live request — used by the console's AI
     * settings "Test connection" button. Overrides bypass the file config so an
     * unsaved key can be checked.
     *
     * @return array{success:bool, message:string}
     */
    public static function ping(string $key = '', string $model = '', string $provider = '', string $baseUrl = ''): array
    {
        $cfg = self::config();
        $provider = $provider ?: (string) ($cfg['provider'] ?? 'anthropic');
        $key = trim($key) ?: trim((string) ($cfg['api_key'] ?? ''));
        if ($key === '') {
            return ['success' => false, 'message' => 'No API key provided.'];
        }
        $r = self::complete('You are a connection test.', 'ping', [
            'provider' => $provider,
            'api_key'  => $key,
            'model'    => $model,
            'base_url' => $baseUrl,
            'max_tokens' => 8,
        ]);
        $meta = self::meta($provider);
        return $r['success']
            ? ['success' => true, 'message' => 'Connection OK — ' . $meta['label'] . ' (' . $r['model'] . ').']
            : ['success' => false, 'message' => $r['error']];
    }

    /** Build [url, headers, payloadArray] for a provider kind. */
    private static function buildRequest(string $kind, string $base, string $key, string $model, int $maxTokens, string $system, string $user, ?float $temp): array
    {
        if ($kind === 'anthropic') {
            $payload = [
                'model' => $model, 'max_tokens' => $maxTokens, 'system' => $system,
                'messages' => [['role' => 'user', 'content' => $user]],
            ];
            if ($temp !== null) $payload['temperature'] = $temp;
            return [$base . '/messages', [
                'content-type: application/json',
                'x-api-key: ' . $key,
                'anthropic-version: ' . self::ANTHROPIC_VERSION,
            ], $payload];
        }
        if ($kind === 'gemini') {
            // key travels as a query param; system + user folded into one turn.
            $payload = [
                'systemInstruction' => ['parts' => [['text' => $system]]],
                'contents' => [['role' => 'user', 'parts' => [['text' => $user]]]],
                'generationConfig' => ['maxOutputTokens' => $maxTokens] + ($temp !== null ? ['temperature' => $temp] : []),
            ];
            return [$base . '/models/' . rawurlencode($model) . ':generateContent?key=' . rawurlencode($key),
                ['content-type: application/json'], $payload];
        }
        // openai-compatible (OpenAI, DeepSeek, Groq, Mistral, OpenRouter, Custom)
        $payload = [
            'model' => $model, 'max_tokens' => $maxTokens,
            'messages' => [['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $user]],
        ];
        if ($temp !== null) $payload['temperature'] = $temp;
        return [$base . '/chat/completions', [
            'content-type: application/json',
            'authorization: Bearer ' . $key,
        ], $payload];
    }

    /** Pull the assistant text out of a provider's response shape. */
    private static function extractText(string $kind, array $json): string
    {
        if ($kind === 'anthropic') {
            $text = '';
            foreach ((array) ($json['content'] ?? []) as $block) {
                if (is_array($block) && ($block['type'] ?? '') === 'text') $text .= (string) ($block['text'] ?? '');
            }
            return trim($text);
        }
        if ($kind === 'gemini') {
            $text = '';
            foreach ((array) ($json['candidates'][0]['content']['parts'] ?? []) as $part) {
                $text .= (string) ($part['text'] ?? '');
            }
            return trim($text);
        }
        return trim((string) ($json['choices'][0]['message']['content'] ?? ''));
    }

    /** Best-effort error message from any provider's error body. */
    private static function errorMessage($json, int $code): string
    {
        if (is_array($json)) {
            $msg = $json['error']['message']   // anthropic / openai
                ?? $json['error']['status']    // gemini
                ?? (is_string($json['error'] ?? null) ? $json['error'] : '');
            if (is_string($msg) && $msg !== '') return $msg;
        }
        return 'HTTP ' . $code;
    }

    /** @return array{0:string|false,1:int,2:string} [body, httpCode, curlError] */
    private static function httpPost(string $url, array $headers, array $payload): array
    {
        if (!\function_exists('curl_init')) {
            return [false, 0, 'cURL extension not available'];
        }
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 60,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        ]);
        $body = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = $body === false ? (string) curl_error($ch) : '';
        curl_close($ch);
        return [$body, $code, $err];
    }
}
