<?php
/**
 * Multi-provider LLM proxy
 *
 * Core of v2 security: the API key never leaves the server
 * Supports OpenRouter, OpenAI, Anthropic, Mistral, Google
 *
 * @package SlideMind
 * @since 2.0.0
 */

namespace SlideMind\Core;

if (!defined('ABSPATH')) {
    exit;
}

class LLMProxy {

    /**
     * LLM provider registry
     * @var array
     */
    private static $providers = [
        'openrouter' => [
            'label'      => 'OpenRouter',
            'api_url'    => 'https://openrouter.ai/api/v1/chat/completions',
            'models_url' => 'https://openrouter.ai/api/v1/models',
            'format'     => 'openai',
            'auth'       => 'bearer',
            'key_pattern' => 'sk-or-...',
            'key_hint'   => 'sk-or-xxxxxxxxxxxxxxxxxxxxxxxx',
            'key_url'    => 'https://openrouter.ai/keys',
        ],
        'anthropic' => [
            'label'      => 'Anthropic (Claude)',
            'api_url'    => 'https://api.anthropic.com/v1/messages',
            'models_url' => 'https://api.anthropic.com/v1/models',
            'format'     => 'anthropic',
            'auth'       => 'x-api-key',
            'key_pattern' => 'sk-ant-...',
            'key_hint'   => 'sk-ant-xxxxxxxxxxxxxxxxxxxxxxxx',
            'key_url'    => 'https://console.anthropic.com/settings/keys',
        ],
        'google' => [
            'label'      => 'Google (Gemini)',
            'api_url'    => 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions',
            'models_url' => 'https://generativelanguage.googleapis.com/v1beta/openai/models',
            'format'     => 'openai',
            'auth'       => 'bearer',
            'key_pattern' => 'AIzaSy...',
            'key_hint'   => 'AIzaSyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
            'key_url'    => 'https://aistudio.google.com/apikey',
        ],
        'mistral' => [
            'label'      => 'Mistral (Mistral)',
            'api_url'    => 'https://api.mistral.ai/v1/chat/completions',
            'models_url' => 'https://api.mistral.ai/v1/models',
            'format'     => 'openai',
            'auth'       => 'bearer',
            'key_pattern' => '(alphanumeric)',
            'key_hint'   => 'xxxxxxxxxxxxxxxxxxxxxxxx',
            'key_url'    => 'https://console.mistral.ai/api-keys',
        ],
        'openai' => [
            'label'      => 'OpenAI (ChatGPT)',
            'api_url'    => 'https://api.openai.com/v1/chat/completions',
            'models_url' => 'https://api.openai.com/v1/models',
            'format'     => 'openai',
            'auth'       => 'bearer',
            'key_pattern' => 'sk-...',
            'key_hint'   => 'sk-xxxxxxxxxxxxxxxxxxxxxxxx',
            'key_url'    => 'https://platform.openai.com/api-keys',
        ],
    ];


    /**
     * API key manager instance
     * @var ApiKeyManager
     */
    private $api_key_manager;

    /**
     * Prompt engine instance
     * @var PromptEngine
     */
    private $prompt_engine;

    /**
     * Learner memory instance (optional)
     * @var LearnerMemory|null
     */
    private $learner_memory;

    /**
     * Constructor
     *
     * @param ApiKeyManager      $api_key_manager
     * @param PromptEngine       $prompt_engine
     * @param LearnerMemory|null $learner_memory
     */
    public function __construct(ApiKeyManager $api_key_manager, PromptEngine $prompt_engine, ?LearnerMemory $learner_memory = null) {
        $this->api_key_manager = $api_key_manager;
        $this->prompt_engine = $prompt_engine;
        $this->learner_memory = $learner_memory;
    }

    /**
     * Returns the provider registry (for the UI)
     *
     * @return array
     */
    public static function get_providers() {
        return self::$providers;
    }

    /**
     * Returns valid provider slugs
     *
     * @return array
     */
    public static function get_provider_slugs() {
        return array_keys(self::$providers);
    }

    /**
     * Sends a chat request to the LLM provider
     *
     * @param array      $config       The conversation configuration
     * @param array      $messages     The chat messages
     * @param array      $variables    The variables
     * @param array|null $learner_info Learner info for memory/profile resolution
     * @return array ['success' => bool, 'data' => array|null, 'error' => string|null]
     */
    public function chat(array $config, array $messages, array $variables = [], ?array $learner_info = null) {
        $start_time = microtime(true);

        // Check the API key
        $api_key = $config['api_key'];
        if (empty($api_key)) {
            return $this->error_response(__('No API key configured', 'slidemind'));
        }

        // Determine the provider
        $provider = isset($config['provider']) && isset(self::$providers[$config['provider']])
            ? $config['provider']
            : 'openrouter';
        $provider_info = self::$providers[$provider];

        // Extract user_id for logging
        $user_id = (!empty($learner_info) && isset($learner_info['user_id'])) ? $learner_info['user_id'] : 0;

        // Resolve learner context (memory/profile) if available
        $learner_context = [];
        if ($learner_info && $this->learner_memory && !empty($config['system_prompt'])) {
            $learner_context = $this->prompt_engine->resolve_learner_context(
                $config['system_prompt'] ?? '',
                $learner_info['user_id'],
                $learner_info['module_id'] ?? null,
                $learner_info['course_id'] ?? null,
                $this->learner_memory,
                $learner_info['allowed_profile_fields'] ?? [],
                $learner_info['session_id'] ?? ''
            );
        }

        // Build the system prompt with variables
        $system_prompt = $this->prompt_engine->build_system_prompt($config, $variables, $learner_context);

        // Format the messages
        $formatted_messages = $this->prompt_engine->format_messages($messages, $system_prompt);

        if (empty($formatted_messages)) {
            return $this->error_response(__('No valid message provided', 'slidemind'));
        }

        // Build and send the request according to the provider format
        if ($provider_info['format'] === 'anthropic') {
            $request = $this->build_request_anthropic($config, $formatted_messages, $api_key, $system_prompt);
        } else {
            $request = $this->build_request_openai($config, $formatted_messages, $api_key, $provider, $provider_info);
        }

        $response = wp_remote_post($provider_info['api_url'], $request);

        $response_time = round((microtime(true) - $start_time) * 1000);

        // Handle connection errors
        if (is_wp_error($response)) {
            $this->log_request($config['id'], 0, 0, 0, $response_time, 'error', $response->get_error_message(), $config['model_id'], $user_id);
            /* translators: %s: WordPress HTTP error message */
            return $this->error_response(sprintf(__('Connection error: %s', 'slidemind'), $response->get_error_message()));
        }

        $status_code = wp_remote_retrieve_response_code($response);
        $body = json_decode(wp_remote_retrieve_body($response), true);

        // Handle HTTP errors
        if ($status_code !== 200) {
            $error_message = $this->extract_error_message($body, $status_code, $provider);
            $this->log_request($config['id'], 0, 0, 0, $response_time, 'error', $error_message, $config['model_id'], $user_id);
            return $this->error_response($error_message);
        }

        // Parse the response according to the format
        if ($provider_info['format'] === 'anthropic') {
            $parsed = $this->parse_response_anthropic($body, $config);
        } else {
            $parsed = $this->parse_response_openai($body, $config);
        }

        if ($parsed === null) {
            /* translators: %s: provider label */
            $error_msg = sprintf(__('Invalid response from %s', 'slidemind'), $provider_info['label']);
            $this->log_request($config['id'], 0, 0, 0, $response_time, 'error', $error_msg, $config['model_id'], $user_id);
            return $this->error_response($error_msg);
        }

        // Log the request
        $this->log_request(
            $config['id'],
            $parsed['usage']['prompt_tokens'],
            $parsed['usage']['completion_tokens'],
            $parsed['usage']['estimated_cost'],
            $response_time,
            'success',
            null,
            $config['model_id'],
            $user_id
        );

        $parsed['response_time'] = $response_time;

        // Parse the structured response if output_variables are defined
        $output_variables = isset($config['output_variables']) ? $config['output_variables'] : [];
        if (!empty($output_variables)) {
            $structured = $this->parse_structured_response($parsed['message']['content'], $output_variables);
            $parsed['structured'] = $structured['valid'];
            $parsed['structured_data'] = $structured['data'];
        }

        return [
            'success' => true,
            'data'    => $parsed,
            'error'   => null,
        ];
    }

    /**
     * Builds a request in OpenAI format (OpenRouter, OpenAI, Mistral)
     */
    private function build_request_openai(array $config, array $messages, string $api_key, string $provider, array $provider_info): array {
        $request_body = [
            'model'       => $config['model_id'],
            'messages'    => $messages,
            'temperature' => isset($config['temperature']) ? (float) $config['temperature'] : 0.7,
            'max_tokens'  => isset($config['max_tokens']) ? (int) $config['max_tokens'] : 1024,
        ];

        $headers = [
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type'  => 'application/json',
        ];

        // OpenRouter-specific headers
        if ($provider === 'openrouter') {
            $headers['HTTP-Referer'] = get_site_url();
            $headers['X-Title'] = get_bloginfo('name');
        }

        return [
            'headers' => $headers,
            'body'    => wp_json_encode($request_body),
            'timeout' => 60,
        ];
    }

    /**
     * Builds a request in Anthropic format
     */
    private function build_request_anthropic(array $config, array $messages, string $api_key, string $system_prompt): array {
        // Separate the system prompt from messages (Anthropic does not accept role=system in messages)
        $api_messages = [];
        foreach ($messages as $msg) {
            if ($msg['role'] !== 'system') {
                $api_messages[] = $msg;
            }
        }

        $request_body = [
            'model'       => $config['model_id'],
            'messages'    => $api_messages,
            'temperature' => isset($config['temperature']) ? (float) $config['temperature'] : 0.7,
            'max_tokens'  => isset($config['max_tokens']) ? (int) $config['max_tokens'] : 1024,
        ];

        // System prompt as top-level field
        if (!empty($system_prompt)) {
            $request_body['system'] = $system_prompt;
        }

        return [
            'headers' => [
                'x-api-key'         => $api_key,
                'anthropic-version'  => '2023-06-01',
                'Content-Type'       => 'application/json',
            ],
            'body'    => wp_json_encode($request_body),
            'timeout' => 60,
        ];
    }

    /**
     * Parses a response in OpenAI format
     *
     * @return array|null
     */
    private function parse_response_openai(array $body, array $config): ?array {
        if (!isset($body['choices'][0]['message'])) {
            return null;
        }

        $usage = isset($body['usage']) ? $body['usage'] : [];
        $prompt_tokens = isset($usage['prompt_tokens']) ? (int) $usage['prompt_tokens'] : 0;
        $completion_tokens = isset($usage['completion_tokens']) ? (int) $usage['completion_tokens'] : 0;

        return [
            'message' => [
                'role'    => $body['choices'][0]['message']['role'],
                'content' => $body['choices'][0]['message']['content'],
            ],
            'usage' => [
                'prompt_tokens'     => $prompt_tokens,
                'completion_tokens' => $completion_tokens,
                'total_tokens'      => $prompt_tokens + $completion_tokens,
                'estimated_cost'    => $this->estimate_cost($config['model_id'], $prompt_tokens, $completion_tokens),
            ],
            'model' => isset($body['model']) ? $body['model'] : $config['model_id'],
        ];
    }

    /**
     * Parses a response in Anthropic format
     *
     * @return array|null
     */
    private function parse_response_anthropic(array $body, array $config): ?array {
        if (!isset($body['content'][0]['text'])) {
            return null;
        }

        $usage = isset($body['usage']) ? $body['usage'] : [];
        $prompt_tokens = isset($usage['input_tokens']) ? (int) $usage['input_tokens'] : 0;
        $completion_tokens = isset($usage['output_tokens']) ? (int) $usage['output_tokens'] : 0;

        return [
            'message' => [
                'role'    => 'assistant',
                'content' => $body['content'][0]['text'],
            ],
            'usage' => [
                'prompt_tokens'     => $prompt_tokens,
                'completion_tokens' => $completion_tokens,
                'total_tokens'      => $prompt_tokens + $completion_tokens,
                'estimated_cost'    => $this->estimate_cost($config['model_id'], $prompt_tokens, $completion_tokens),
            ],
            'model' => isset($body['model']) ? $body['model'] : $config['model_id'],
        ];
    }

    /**
     * Retrieves the list of models for a provider
     *
     * @param string $provider Provider slug (default: global provider)
     * @return array List of models [{id, name}, ...]
     */
    public function get_models($provider = null) {
        if ($provider === null) {
            $provider = get_option('slidemind_global_provider', 'openrouter');
        }

        if (!isset(self::$providers[$provider])) {
            return [];
        }

        $cache_key = 'slidemind_' . $provider . '_models';
        $cached = get_transient($cache_key);
        if ($cached !== false) {
            return $cached;
        }

        $models_url = self::$providers[$provider]['models_url'];
        if (empty($models_url)) {
            return [];
        }

        // All providers (except OpenRouter) require a key
        $api_key = $this->api_key_manager->get_global_api_key($provider);
        if (empty($api_key) && $provider !== 'openrouter') {
            return [];
        }

        // Authentication headers according to the provider
        $headers = [];
        if (!empty($api_key)) {
            if (self::$providers[$provider]['auth'] === 'x-api-key') {
                $headers['x-api-key']         = $api_key;
                $headers['anthropic-version']  = '2023-06-01';
            } else {
                $headers['Authorization'] = 'Bearer ' . $api_key;
            }
        }

        $response = wp_remote_get($models_url, [
            'headers' => $headers,
            'timeout' => 15,
        ]);

        if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
            return [];
        }

        $body = json_decode(wp_remote_retrieve_body($response), true);
        $raw_models = isset($body['data']) ? $body['data'] : [];

        // Normalize the structure
        $models = [];
        foreach ($raw_models as $m) {
            $id = isset($m['id']) ? $m['id'] : '';
            // Anthropic uses display_name, others use name
            $name = isset($m['name']) ? $m['name'] : (isset($m['display_name']) ? $m['display_name'] : $id);

            if (empty($id)) {
                continue;
            }

            // Filter non-chat models for OpenAI/Mistral/Google
            if ($provider === 'openai' || $provider === 'mistral') {
                // Exclude embedding, tts, whisper, dall-e models, etc.
                if (preg_match('/^(text-|embed|tts-|whisper|dall-e|davinci|babbage|curie|ada|ft:)/i', $id)) {
                    continue;
                }
            }
            if ($provider === 'google') {
                // Exclude embedding, AQA, imagen models, etc.
                if (preg_match('/(embed|aqa|imagen|text-bison|chat-bison)/i', $id)) {
                    continue;
                }
            }

            $entry = ['id' => $id, 'name' => $name];

            if (isset($m['pricing'])) {
                $entry['pricing'] = $m['pricing'];
            }

            $models[] = $entry;
        }

        if (empty($models)) {
            return [];
        }

        // Sort by name
        usort($models, function ($a, $b) {
            return strcmp($a['name'], $b['name']);
        });

        // Cache for 12 hours
        set_transient($cache_key, $models, 12 * HOUR_IN_SECONDS);

        return $models;
    }

    /**
     * Estimates the cost of a request
     *
     * @param string $model_id
     * @param int $prompt_tokens
     * @param int $completion_tokens
     * @return float Estimated cost in USD
     */
    private function estimate_cost($model_id, $prompt_tokens, $completion_tokens) {
        // Try dynamic pricing from cached model data (OpenRouter transient)
        $price = $this->get_dynamic_pricing($model_id);

        if ($price === null) {
            // Fallback: hardcoded pricing ($/M tokens) for common models
            $fallback_pricing = [
                'openai/gpt-4-turbo'           => ['prompt' => 10, 'completion' => 30],
                'openai/gpt-4'                 => ['prompt' => 30, 'completion' => 60],
                'openai/gpt-3.5-turbo'         => ['prompt' => 0.5, 'completion' => 1.5],
                'anthropic/claude-3-opus'      => ['prompt' => 15, 'completion' => 75],
                'anthropic/claude-3-sonnet'    => ['prompt' => 3, 'completion' => 15],
                'anthropic/claude-3-haiku'     => ['prompt' => 0.25, 'completion' => 1.25],
                'anthropic/claude-3.5-sonnet'  => ['prompt' => 3, 'completion' => 15],
                'google/gemini-pro'            => ['prompt' => 0.5, 'completion' => 1.5],
                'meta-llama/llama-3-70b'       => ['prompt' => 0.9, 'completion' => 0.9],
            ];

            $price = isset($fallback_pricing[$model_id]) ? $fallback_pricing[$model_id] : ['prompt' => 1, 'completion' => 2];
        }

        $prompt_cost = ($prompt_tokens / 1000000) * $price['prompt'];
        $completion_cost = ($completion_tokens / 1000000) * $price['completion'];

        return round($prompt_cost + $completion_cost, 6);
    }

    /**
     * Retrieves dynamic pricing from cached model list (transient)
     *
     * OpenRouter API returns pricing per token; we convert to $/M tokens
     * to match the format used by estimate_cost().
     *
     * @param string $model_id
     * @return array|null ['prompt' => float, 'completion' => float] in $/M tokens, or null
     */
    private function get_dynamic_pricing($model_id) {
        // Check all provider transients
        $providers = array_keys(self::$providers);

        foreach ($providers as $provider) {
            $cache_key = 'slidemind_' . $provider . '_models';
            $cached = get_transient($cache_key);

            if ($cached === false || !is_array($cached)) {
                continue;
            }

            foreach ($cached as $model) {
                if ($model['id'] !== $model_id || empty($model['pricing'])) {
                    continue;
                }

                $prompt_per_token = isset($model['pricing']['prompt']) ? (float) $model['pricing']['prompt'] : 0;
                $completion_per_token = isset($model['pricing']['completion']) ? (float) $model['pricing']['completion'] : 0;

                if ($prompt_per_token <= 0 && $completion_per_token <= 0) {
                    continue;
                }

                // Convert from $/token to $/M tokens
                return [
                    'prompt'     => $prompt_per_token * 1000000,
                    'completion' => $completion_per_token * 1000000,
                ];
            }
        }

        return null;
    }

    /**
     * Extracts the error message from the response
     *
     * @param array|null $body
     * @param int $status_code
     * @param string $provider
     * @return string
     */
    private function extract_error_message($body, $status_code, $provider = 'openrouter') {
        // Anthropic: different format
        if ($provider === 'anthropic' && isset($body['error']['message'])) {
            return $body['error']['message'];
        }

        if (isset($body['error']['message'])) {
            return $body['error']['message'];
        }

        $provider_label = isset(self::$providers[$provider]) ? self::$providers[$provider]['label'] : $provider;

        switch ($status_code) {
            case 401:
                return __('Invalid or expired API key', 'slidemind');
            case 402:
                /* translators: %s: provider name */
                return sprintf(__('Insufficient credit on the %s account', 'slidemind'), $provider_label);
            case 429:
                return __('Rate limit exceeded, please try again later', 'slidemind');
            case 500:
            case 502:
            case 503:
                /* translators: %s: provider name */
                return sprintf(__('%s server error, please try again later', 'slidemind'), $provider_label);
            default:
                /* translators: %d: HTTP status code */
                return sprintf(__('HTTP error %d', 'slidemind'), $status_code);
        }
    }

    /**
     * Creates a standardized error response
     *
     * @param string $message
     * @return array
     */
    private function error_response($message) {
        return [
            'success' => false,
            'data'    => null,
            'error'   => $message,
        ];
    }

    /**
     * Logs a request in the logs table
     *
     * @param int $config_id
     * @param int $request_tokens
     * @param int $response_tokens
     * @param float $cost
     * @param int $response_time_ms
     * @param string $status
     * @param string|null $error_message
     * @param string $model_id
     * @param int $user_id
     */
    private function log_request($config_id, $request_tokens, $response_tokens, $cost, $response_time_ms, $status, $error_message, $model_id, $user_id = 0) {
        global $wpdb;

        $table = $wpdb->prefix . 'slidemind_logs';

        // Fallback to the logged-in WP user (covers admin sandbox)
        if (empty($user_id)) {
            $user_id = get_current_user_id();
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        $wpdb->insert($table, [
            'config_id'       => $config_id,
            'request_tokens'  => $request_tokens,
            'response_tokens' => $response_tokens,
            'total_tokens'    => $request_tokens + $response_tokens,
            'estimated_cost'  => $cost,
            'response_time_ms'=> $response_time_ms,
            'model_id'        => $model_id,
            'status'          => $status,
            'error_message'   => $error_message,
            'user_ip'         => $this->get_client_ip(),
            'user_id'         => (int) $user_id,
            'created_at'      => current_time('mysql'),
        ], [
            '%d', '%d', '%d', '%d', '%f', '%d', '%s', '%s', '%s', '%s', '%d', '%s',
        ]);
    }

    /**
     * Retrieves the client IP
     *
     * @return string
     */
    private function get_client_ip() {
        $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];

        foreach ($ip_keys as $key) {
            if (!empty($_SERVER[$key])) {
                $ip = explode(',', sanitize_text_field(wp_unslash($_SERVER[$key])))[0];
                $ip = trim($ip);
                if (filter_var($ip, FILTER_VALIDATE_IP)) {
                    return $ip;
                }
            }
        }

        return '';
    }

    // =========================================================================
    // STRUCTURED RESPONSE PARSING (OUTPUT VARIABLES)
    // =========================================================================

    /**
     * Parses and validates a structured LLM response
     *
     * @param string $content The raw LLM response content
     * @param array $output_variables The expected output variables
     * @return array ['valid' => bool, 'data' => array]
     */
    private function parse_structured_response(string $content, array $output_variables): array {
        $json = $this->extract_json_from_response($content);

        if ($json === null) {
            return [
                'valid' => false,
                'data'  => $this->build_fallback_response($content, $output_variables),
            ];
        }

        // Validate and cast each field
        $data = [];
        foreach ($output_variables as $var) {
            $name = $var['name'];

            if (array_key_exists($name, $json)) {
                $data[$name] = $this->validate_and_cast($json[$name], $var);
            } else {
                $data[$name] = $this->get_default_for_type($var['type']);
            }
        }

        return [
            'valid' => true,
            'data'  => $data,
        ];
    }

    /**
     * Extracts JSON from an LLM response (pure, wrapped in ```json, or mixed in text)
     *
     * @param string $content
     * @return array|null The decoded array or null if invalid
     */
    private function extract_json_from_response(string $content): ?array {
        $content = trim($content);

        // Attempt 1: Pure JSON
        $decoded = json_decode($content, true, 64);
        if (is_array($decoded) && json_last_error() === JSON_ERROR_NONE) {
            return $decoded;
        }

        // Attempt 2: JSON wrapped in ```json ... ```
        if (preg_match('/```(?:json)?\s*\n?(.*?)\n?\s*```/s', $content, $matches)) {
            $decoded = json_decode(trim($matches[1]), true, 64);
            if (is_array($decoded) && json_last_error() === JSON_ERROR_NONE) {
                return $decoded;
            }
        }

        // Attempt 3: First JSON object found in the text
        if (preg_match('/\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/s', $content, $matches)) {
            $decoded = json_decode($matches[0], true, 64);
            if (is_array($decoded) && json_last_error() === JSON_ERROR_NONE) {
                return $decoded;
            }
        }

        return null;
    }

    /**
     * Validates and casts a value according to the expected variable type
     *
     * @param mixed $value The raw value
     * @param array $var The variable definition
     * @return mixed The cast value
     */
    private function validate_and_cast($value, array $var) {
        $type = $var['type'];

        switch ($type) {
            case 'nombre':
                if (is_numeric($value)) {
                    return strpos((string) $value, '.') !== false ? (float) $value : (int) $value;
                }
                return $this->get_default_for_type('nombre');

            case 'booleen':
                if (is_bool($value)) {
                    return $value;
                }
                if (is_string($value)) {
                    $lower = strtolower($value);
                    if (in_array($lower, ['true', '1', 'oui', 'yes'], true)) {
                        return true;
                    }
                    if (in_array($lower, ['false', '0', 'non', 'no'], true)) {
                        return false;
                    }
                }
                return $this->get_default_for_type('booleen');

            case 'enum':
                $allowed = isset($var['values']) ? $var['values'] : [];
                if (is_string($allowed)) {
                    $allowed = array_map('trim', explode(',', $allowed));
                }
                $str_value = (string) $value;
                if (in_array($str_value, $allowed, true)) {
                    return $str_value;
                }
                // Fallback to the first allowed value
                return !empty($allowed) ? $allowed[0] : '';

            case 'texte':
            default:
                return (string) $value;
        }
    }

    /**
     * Returns the default value for a variable type
     *
     * @param string $type
     * @return mixed
     */
    private function get_default_for_type(string $type) {
        switch ($type) {
            case 'nombre':
                return 0;
            case 'booleen':
                return false;
            case 'texte':
            default:
                return '';
        }
    }

    /**
     * Builds a fallback response when JSON is invalid
     *
     * Places the raw content in the first text field or in response_text
     *
     * @param string $content The raw content
     * @param array $output_variables The expected variables
     * @return array
     */
    private function build_fallback_response(string $content, array $output_variables): array {
        $data = [];
        $text_field_found = false;

        foreach ($output_variables as $var) {
            $name = $var['name'];
            $type = $var['type'];

            // Place the raw content in the first text field encountered
            if ($type === 'texte' && !$text_field_found) {
                $data[$name] = $content;
                $text_field_found = true;
            } else {
                $data[$name] = $this->get_default_for_type($type);
            }
        }

        // If no text field, add response_text
        if (!$text_field_found) {
            $data['response_text'] = $content;
        }

        return $data;
    }
}
