<?php
/**
 * Reusable Gemini transport and base response service for Social Publisher.
 *
 * This class deliberately contains no Pro/Content Variations knowledge. It owns
 * the shared Gemini configuration, HTTP transport, model fallback and base
 * response parsing so Free and optional extensions use one contract.
 */
if (!defined('ABSPATH')) { exit; }

final class Calliope_SP_AI_Service {
    private static ?self $instance = null;
    private ?WP_Error $last_error = null;

    public static function instance(): self {
        if (self::$instance === null) { self::$instance = new self(); }
        return self::$instance;
    }

    /** @return array<string,string> */
    public static function model_choices(): array {
        return [
            'gemini-3.1-flash-lite'  => __('Gemini 3.1 Flash-Lite — Recommended · stable / high volume', 'calliope-social-publisher'),
            'gemini-3.5-flash-lite'  => __('Gemini 3.5 Flash-Lite — Stable · fast and efficient', 'calliope-social-publisher'),
            'gemini-3.5-flash'       => __('Gemini 3.5 Flash — Stable · higher quality', 'calliope-social-publisher'),
            'gemini-3.6-flash'       => __('Gemini 3.6 Flash — Latest stable Flash', 'calliope-social-publisher'),
            'gemini-2.5-flash'       => __('Gemini 2.5 Flash — Stable fallback', 'calliope-social-publisher'),
            'gemini-2.5-flash-lite'  => __('Gemini 2.5 Flash-Lite — Stable budget fallback', 'calliope-social-publisher'),
            'gemini-2.5-pro'         => __('Gemini 2.5 Pro — Higher quality / may require billing', 'calliope-social-publisher'),
            'gemini-3.1-pro-preview' => __('Gemini 3.1 Pro Preview — Preview / billing may be required', 'calliope-social-publisher'),
            'gemini-3-flash-preview' => __('Gemini 3 Flash Preview — Preview model', 'calliope-social-publisher'),
        ];
    }

    public static function normalize_model($model): string {
        $model = trim((string) $model);
        if ($model === '') { return 'gemini-3.1-flash-lite'; }
        $compat = [
            'gemini-3.1-flash-lite-preview' => 'gemini-3.1-flash-lite',
            'gemini-1.5-flash' => 'gemini-3.1-flash-lite',
            'gemini-1.5-pro' => 'gemini-2.5-pro',
            'gemini-2.0-flash' => 'gemini-3.1-flash-lite',
            'gemini-2.0-flash-001' => 'gemini-3.1-flash-lite',
            'gemini-2.0-flash-lite' => 'gemini-2.5-flash-lite',
            'gemini-2.0-flash-lite-001' => 'gemini-2.5-flash-lite',
            'gemini-2.0-flash-lite-preview' => 'gemini-2.5-flash-lite',
            'gemini-2.0-flash-lite-preview-02-05' => 'gemini-2.5-flash-lite',
            'gemini-2.5-flash-preview-09-25' => 'gemini-3.1-flash-lite',
            'gemini-2.5-flash-lite-preview-06-17' => 'gemini-2.5-flash-lite',
            'gemini-2.5-flash-lite-preview-09-2025' => 'gemini-2.5-flash-lite',
            'gemini-2.5-pro-preview-03-25' => 'gemini-2.5-pro',
            'gemini-2.5-pro-preview-05-06' => 'gemini-2.5-pro',
            'gemini-2.5-pro-preview-06-05' => 'gemini-2.5-pro',
            'gemini-3-pro-preview' => 'gemini-3.1-pro-preview',
            'gemini-pro-latest' => 'gemini-3.1-pro-preview',
            'gemini-flash-latest' => 'gemini-3.5-flash',
        ];
        return $compat[$model] ?? $model;
    }

    public static function sanitize_model($model): string {
        $model = self::normalize_model($model);
        return array_key_exists($model, self::model_choices()) ? $model : 'gemini-3.1-flash-lite';
    }

    public function is_configured(): bool { return $this->get_api_key() !== ''; }
    public function get_api_key(): string { return trim((string) get_option('calliope_sp_gemini_api_key', '')); }
    public function get_model(): string { return self::sanitize_model(get_option('calliope_sp_gemini_model', 'gemini-3.1-flash-lite')); }
    public function get_last_error(): ?WP_Error { return $this->last_error; }

    /**
     * Generic text generation helper for extensions.
     *
     * @param string $prompt
     * @param array<string,mixed> $args
     * @return string|WP_Error
     */
    public function generate_text(string $prompt, array $args = []) {
        $prompt = trim($prompt);
        if ($prompt === '') { return new WP_Error('calliope_sp_ai_empty_prompt', __('AI generation needs context.', 'calliope-social-publisher')); }
        $config = [
            'temperature' => isset($args['temperature']) ? (float) $args['temperature'] : 0.55,
            'topP' => isset($args['top_p']) ? (float) $args['top_p'] : 0.9,
            'maxOutputTokens' => isset($args['max_output_tokens']) ? max(128, min(8192, (int) $args['max_output_tokens'])) : 2200,
        ];
        if (!empty($args['response_schema']) && is_array($args['response_schema'])) {
            $config['responseMimeType'] = 'application/json';
            $config['responseSchema'] = $args['response_schema'];
        }
        $payload = [
            'contents' => [['role' => 'user', 'parts' => [['text' => $prompt]]]],
            'generationConfig' => $config,
        ];
        if (!empty($args['tools']) && is_array($args['tools'])) { $payload['tools'] = $args['tools']; }
        $raw = $this->request_raw($payload);
        if (is_wp_error($raw)) { return $raw; }
        return $this->parse_response((string) $raw['body'], (int) $raw['upstream_status'], !empty($args['requires_url_context']));
    }

    /** @return array<mixed>|WP_Error */
    public function generate_structured(string $prompt, array $schema, array $args = []) {
        $args['response_schema'] = $schema;
        $text = $this->generate_text($prompt, $args);
        if (is_wp_error($text)) { return $text; }
        $decoded = $this->decode_structured_json($text);
        if (!is_array($decoded)) {
            return new WP_Error('calliope_sp_ai_invalid_json', __('The generated response could not be parsed.', 'calliope-social-publisher'), ['http_status'=>502,'retryable'=>true]);
        }
        return $decoded;
    }

    /**
     * Execute a generateContent payload with the configured key/model.
     * A test filter may short-circuit the HTTP response without exposing secrets.
     *
     * @return array{body:string,upstream_status:int,model:string}|WP_Error
     */
    public function request_raw(array $payload) {
        $this->last_error = null;
        $api_key = $this->get_api_key();
        if ($api_key === '') {
            return $this->remember(new WP_Error('calliope_sp_ai_not_configured', __('Add your Gemini API key in Settings before using AI generation.', 'calliope-social-publisher'), ['http_status'=>400,'retryable'=>false]));
        }
        $models = array_values(array_unique(array_filter([$this->get_model(), 'gemini-3.1-flash-lite', 'gemini-2.5-flash-lite'])));
        // Extensions may impose a stricter per-operation transport budget while
        // keeping the shared AI transport as the single source of truth. Zero
        // preserves the historical retry/fallback behaviour.
        $max_http_attempts = max(0, min(10, (int) apply_filters('calliope_sp_ai_service_max_http_attempts', 0, $payload)));
        $http_attempts = 0;
        $last_error = null;
        $last_success = null;
        foreach ($models as $index => $model) {
            $attempts = $index === 0 ? 2 : 1;
            for ($attempt = 0; $attempt < $attempts; $attempt++) {
                if ($max_http_attempts > 0 && $http_attempts >= $max_http_attempts) { break 2; }
                $http_attempts++;
                $request_payload = $this->prepare_payload_for_model($payload, $model);
                $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($model) . ':generateContent';
                $request_args = [
                    'headers' => ['Content-Type'=>'application/json; charset=utf-8','x-goog-api-key'=>$api_key],
                    'body' => wp_json_encode($request_payload),
                    'timeout' => (int) apply_filters('calliope_sp_ai_service_timeout', 45),
                ];
                $response = apply_filters('calliope_sp_ai_service_pre_http_response', null, $url, $request_args, $model);
                if ($response === null) { $response = wp_remote_post($url, $request_args); }
                if (is_wp_error($response)) {
                    $last_error = new WP_Error('calliope_sp_gemini_transport_error', __('Gemini could not be reached. Please try again later.', 'calliope-social-publisher'), ['http_status'=>502,'upstream_status'=>0,'retryable'=>true]);
                    continue;
                }
                $status = (int) wp_remote_retrieve_response_code($response);
                $body = (string) wp_remote_retrieve_body($response);
                if ($status < 200 || $status >= 300) {
                    $data = json_decode($body, true);
                    $last_error = $this->build_http_error($status, is_array($data) ? $data : []);
                    if (!$this->should_try_fallback($last_error)) { return $this->remember($last_error); }
                    break;
                }
                $last_success = ['body'=>$body,'upstream_status'=>$status,'model'=>$model];
                if (!$this->success_body_needs_retry($body)) { return $last_success; }
            }
        }
        if (is_array($last_success)) { return $last_success; }
        if (is_wp_error($last_error)) { return $this->remember($last_error); }
        return $this->remember(new WP_Error('calliope_sp_gemini_unavailable', __('Gemini is temporarily unavailable. Please try again later.', 'calliope-social-publisher'), ['http_status'=>503,'upstream_status'=>0,'retryable'=>true]));
    }

    /** @return string|WP_Error */
    public function parse_response(string $body, int $upstream_status, bool $requires_url_context = false) {
        $data = json_decode($body, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
            return $this->remember($this->parser_error('calliope_sp_gemini_invalid_response', __('Gemini returned a response that could not be understood. Please try again.', 'calliope-social-publisher'), 502, $upstream_status, true));
        }
        $block = '';
        if (isset($data['promptFeedback']['blockReason']) && is_string($data['promptFeedback']['blockReason'])) { $block = $this->sanitize_provider_status($data['promptFeedback']['blockReason']); }
        if ($block !== '') { return $this->remember($this->parser_error('calliope_sp_gemini_prompt_blocked', __('Gemini could not generate this content because the request was blocked by its safety filters.', 'calliope-social-publisher'), 422, $upstream_status, false, $block)); }
        if (empty($data['candidates']) || !is_array($data['candidates'])) { return $this->remember($this->parser_error('calliope_sp_gemini_no_candidates', __('Gemini did not return a usable response. Please try again.', 'calliope-social-publisher'), 502, $upstream_status, true)); }
        $pending = null; $priority = 0; $url_status = '';
        $blocked = ['SAFETY','BLOCKLIST','PROHIBITED_CONTENT','SPII','RECITATION','IMAGE_SAFETY'];
        foreach ($data['candidates'] as $candidate) {
            if (!is_array($candidate)) { continue; }
            $finish = isset($candidate['finishReason']) && is_string($candidate['finishReason']) ? $this->sanitize_provider_status($candidate['finishReason']) : '';
            if (in_array($finish, $blocked, true)) { return $this->remember($this->parser_error('calliope_sp_gemini_candidate_blocked', __('Gemini could not return this content because it was blocked by its safety filters.', 'calliope-social-publisher'), 422, $upstream_status, false, $finish)); }
            $candidate_url = $requires_url_context ? $this->extract_url_context_status($candidate) : '';
            if ($requires_url_context && $candidate_url !== 'URL_RETRIEVAL_STATUS_SUCCESS') { if ($url_status === '' && $candidate_url !== '') { $url_status = $candidate_url; } continue; }
            if ($finish === 'MAX_TOKENS') { if ($priority < 30) { $pending = $this->parser_error('calliope_sp_gemini_truncated', __('Gemini stopped before completing the response. Please shorten the context and try again.', 'calliope-social-publisher'), 502, $upstream_status, true, $finish); $priority=30; } continue; }
            if ($finish !== '' && $finish !== 'STOP') { if ($priority < 20) { $pending = $this->parser_error('calliope_sp_gemini_incomplete_response', __('Gemini could not complete the response. Please try again.', 'calliope-social-publisher'), 502, $upstream_status, true, $finish); $priority=20; } continue; }
            $parts = $this->extract_text_parts($candidate);
            if ($parts !== []) { return implode("\n", $parts); }
            if ($priority < 10) { $pending = $this->parser_error('calliope_sp_gemini_empty', __('Gemini returned an empty response. Please try again.', 'calliope-social-publisher'), 502, $upstream_status, true); $priority=10; }
        }
        if ($requires_url_context && (!is_wp_error($pending) || $priority < 20)) { return $this->remember($this->parser_error('calliope_sp_gemini_url_context_failed', __('The linked page could not be read from WordPress or Gemini. Check that it is publicly accessible, or add a short AI prompt describing what you want to publish.', 'calliope-social-publisher'), 422, $upstream_status, false, $url_status)); }
        if (is_wp_error($pending)) { return $this->remember($pending); }
        return $this->remember($this->parser_error('calliope_sp_gemini_empty', __('Gemini returned an empty response. Please try again.', 'calliope-social-publisher'), 502, $upstream_status, true));
    }

    /** @return array<mixed>|null */
    public function decode_structured_json(string $text): ?array {
        $text = trim($text);
        if ($text === '') { return null; }
        if (preg_match('/^```(?:json)?\s*(.*?)\s*```$/is', $text, $m)) { $text = trim((string) $m[1]); }
        $decoded = json_decode($text, true);
        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { return $decoded; }
        $start = strpos($text, '{'); $end = strrpos($text, '}');
        if ($start !== false && $end !== false && $end > $start) {
            $decoded = json_decode(substr($text, $start, $end - $start + 1), true);
            if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { return $decoded; }
        }
        return null;
    }

    private function prepare_payload_for_model(array $payload, string $model): array {
        if (!isset($payload['generationConfig']) || !is_array($payload['generationConfig'])) { $payload['generationConfig'] = []; }
        if (in_array($model, ['gemini-3.6-flash','gemini-3.5-flash-lite'], true)) { unset($payload['generationConfig']['temperature'],$payload['generationConfig']['topP'],$payload['generationConfig']['topK']); }
        if (strpos($model, 'gemini-3') === 0) { $payload['generationConfig']['thinkingConfig'] = ['thinkingLevel'=>'low']; }
        elseif ($model === 'gemini-2.5-pro') { $payload['generationConfig']['thinkingConfig'] = ['thinkingBudget'=>128]; }
        elseif (strpos($model, 'gemini-2.5-') === 0) { $payload['generationConfig']['thinkingConfig'] = ['thinkingBudget'=>0]; }
        return $payload;
    }

    private function should_try_fallback(WP_Error $error): bool {
        $code = (string) $error->get_error_code();
        if (in_array($code, ['calliope_sp_gemini_model_not_found','calliope_sp_gemini_transport_error','calliope_sp_gemini_timeout','calliope_sp_gemini_server_error','calliope_sp_gemini_bad_gateway','calliope_sp_gemini_unavailable','calliope_sp_gemini_gateway_timeout'], true)) { return true; }
        $data = $error->get_error_data();
        return is_array($data) && !empty($data['retryable']) && $code !== 'calliope_sp_gemini_rate_limited';
    }

    private function success_body_needs_retry(string $body): bool {
        $data = json_decode($body, true);
        if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { return false; }
        if (!empty($data['promptFeedback']['blockReason'])) { return false; }
        if (empty($data['candidates']) || !is_array($data['candidates'])) { return true; }
        $blocked = ['SAFETY','BLOCKLIST','PROHIBITED_CONTENT','SPII','RECITATION','IMAGE_SAFETY'];
        foreach ($data['candidates'] as $candidate) {
            if (!is_array($candidate)) { continue; }
            $finish = isset($candidate['finishReason']) && is_string($candidate['finishReason']) ? $this->sanitize_provider_status($candidate['finishReason']) : '';
            if (in_array($finish, $blocked, true)) { return false; }
            if ($this->extract_text_parts($candidate) !== []) { return false; }
        }
        return true;
    }

    /** @return string[] */
    private function extract_text_parts(array $candidate): array {
        if (empty($candidate['content']['parts']) || !is_array($candidate['content']['parts'])) { return []; }
        $parts=[]; foreach ($candidate['content']['parts'] as $part) { if (is_array($part) && isset($part['text']) && is_string($part['text']) && trim($part['text']) !== '') { $parts[] = trim($part['text']); } }
        return $parts;
    }

    private function extract_url_context_status(array $candidate): string {
        $metadata = isset($candidate['url_context_metadata']) && is_array($candidate['url_context_metadata']) ? $candidate['url_context_metadata'] : (isset($candidate['urlContextMetadata']) && is_array($candidate['urlContextMetadata']) ? $candidate['urlContextMetadata'] : null);
        if (!is_array($metadata)) { return ''; }
        $items = isset($metadata['url_metadata']) && is_array($metadata['url_metadata']) ? $metadata['url_metadata'] : (isset($metadata['urlMetadata']) && is_array($metadata['urlMetadata']) ? $metadata['urlMetadata'] : []);
        $first=''; foreach ($items as $item) { if (!is_array($item)) { continue; } $url=(string)($item['retrieved_url']??$item['retrievedUrl']??''); $status=$this->sanitize_provider_status((string)($item['url_retrieval_status']??$item['urlRetrievalStatus']??'')); if ($status==='') { continue; } if (trim($url)!=='' && $status==='URL_RETRIEVAL_STATUS_SUCCESS') { return $status; } if ($status!=='URL_RETRIEVAL_STATUS_SUCCESS' && $first==='') { $first=$status; } }
        return $first;
    }

    private function build_http_error(int $status, array $body): WP_Error {
        $provider = isset($body['error']) && is_array($body['error']) ? $body['error'] : [];
        $provider_status = $this->sanitize_provider_status((string)($provider['status'] ?? ''));
        $provider_code = isset($provider['code']) ? (int)$provider['code'] : 0;
        $known = [
            400=>['calliope_sp_gemini_bad_request',__('Gemini rejected the request. Please review the supplied content and try again.', 'calliope-social-publisher'),false],
            401=>['calliope_sp_gemini_unauthorized',__('The Gemini API key is invalid or could not be authenticated.', 'calliope-social-publisher'),false],
            403=>['calliope_sp_gemini_forbidden',__('The Gemini project does not have permission to use this model. Check the API key, project access, region, and billing settings.', 'calliope-social-publisher'),false],
            404=>['calliope_sp_gemini_model_not_found',__('The selected Gemini model is not available for this project.', 'calliope-social-publisher'),false],
            408=>['calliope_sp_gemini_timeout',__('Gemini took too long to respond. Please try again.', 'calliope-social-publisher'),true],
            429=>['calliope_sp_gemini_rate_limited',__('Gemini’s usage limit has been reached. Please wait and try again.', 'calliope-social-publisher'),true],
            500=>['calliope_sp_gemini_server_error',__('Gemini encountered an internal error. Please try again later.', 'calliope-social-publisher'),true],
            502=>['calliope_sp_gemini_bad_gateway',__('Gemini returned an invalid upstream response. Please try again later.', 'calliope-social-publisher'),true],
            503=>['calliope_sp_gemini_unavailable',__('Gemini is temporarily unavailable. Please try again later.', 'calliope-social-publisher'),true],
            504=>['calliope_sp_gemini_gateway_timeout',__('Gemini did not respond in time. Please try again later.', 'calliope-social-publisher'),true],
        ];
        if (isset($known[$status])) { [$code,$message,$retryable]=$known[$status]; $http=$status; }
        else { $http=($status>=400&&$status<=599)?$status:502; $code='calliope_sp_gemini_http_error'; $message=__('Gemini returned an unexpected error. Please try again later.', 'calliope-social-publisher'); $retryable=$http===408||$http===429||$http>=500; }
        $data=['http_status'=>$http,'upstream_status'=>$status,'retryable'=>(bool)$retryable]; if ($provider_status!==''){$data['provider_status']=$provider_status;} if($provider_code>0){$data['provider_code']=$provider_code;}
        return new WP_Error($code,$message,$data);
    }

    private function parser_error(string $code,string $message,int $http,int $upstream,bool $retryable,string $provider=''): WP_Error {
        $data=['http_status'=>$http,'upstream_status'=>$upstream,'retryable'=>$retryable]; $provider=$this->sanitize_provider_status($provider); if($provider!==''){$data['provider_status']=$provider;} return new WP_Error($code,$message,$data);
    }
    private function sanitize_provider_status(string $status): string { $status=trim($status); return $status!==''&&strlen($status)<=80&&preg_match('/^[A-Z0-9_]+$/',$status)?$status:''; }
    private function remember(WP_Error $error): WP_Error { $this->last_error=$error; return $error; }
}
