<?php

namespace WPSN\Modules\AI;

use WPSN\Core\Http\HttpClient;
use WPSN\Core\Logger;

defined('ABSPATH') || exit;

final class OpenAIClient
{
    private string $apiKey;
    private string $organization;
    private string $project;
    private Logger $logger;

    public function __construct(string $apiKey, string $organization, string $project, Logger $logger)
    {
        $this->apiKey = trim($apiKey);
        $this->organization = trim($organization);
        $this->project = trim($project);
        $this->logger = $logger;
    }

    /**
     * @return array{ok:bool,text:string,usage:array{input:int,output:int,total:int},latency_ms:int,error:string,http_status:int,retry_after:int}
     */
    public function generateText(string $model, string $input, int $maxOutputTokens, float $temperature): array
    {
        return $this->generate($model, $input, $maxOutputTokens, $temperature);
    }

    /**
     * Responses API helper.
     *
     * Accepts either a plain string input or a structured input array (for vision/multi-part inputs).
     *
     * @param string|array<int, array<string,mixed>> $input
     * @return array{ok:bool,text:string,usage:array{input:int,output:int,total:int},latency_ms:int,error:string,http_status:int,retry_after:int}
     */
    public function generate(string $model, $input, int $maxOutputTokens, float $temperature): array
    {
        $t0 = microtime(true);

        $headers = [
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . $this->apiKey,
            // Mark for the Privacy module external request log.
            'X-WPSN-Service' => 'openai',
            'X-WPSN-Context' => 'responses',
        ];

        if ($this->organization !== '') {
            $headers['OpenAI-Organization'] = $this->organization;
        }
        if ($this->project !== '') {
            $headers['OpenAI-Project'] = $this->project;
        }

        $payload = [
            'model' => $model,
            'input' => $input,
            'max_output_tokens' => max(16, $maxOutputTokens),
            'temperature' => max(0.0, min(2.0, $temperature)),
        ];

        $resp = HttpClient::post('https://api.openai.com/v1/responses', [
            'headers' => $headers,
            'user-agent' => 'WPSN/' . (defined('WPSN_VERSION') ? WPSN_VERSION : 'dev'),
            'timeout' => 60,
            'body'    => wp_json_encode($payload),
        ], [
            'service' => 'openai',
            'context' => 'responses',
            // Avoid retrying 429 in-request; use circuit/backoff instead.
            'retry_statuses' => [500, 502, 503, 504],
            'retries' => 1,
        ]);

        $latency = (int) round((microtime(true) - $t0) * 1000);

        if (is_wp_error($resp)) {
            $retryAfter = 0;
            $data = $resp->get_error_data();
            if (is_array($data) && isset($data['retry_after'])) {
                $retryAfter = (int) $data['retry_after'];
            }
            return [
                'ok' => false,
                'text' => '',
                'usage' => ['input' => 0, 'output' => 0, 'total' => 0],
                'latency_ms' => $latency,
                'error' => $resp->get_error_message(),
                'http_status' => 0,
                'retry_after' => $retryAfter,
            ];
        }

        $code = (int) wp_remote_retrieve_response_code($resp);
        $body = (string) wp_remote_retrieve_body($resp);

        $retryAfter = 0;
        $respHeaders = wp_remote_retrieve_headers($resp);
        if (is_array($respHeaders)) {
            foreach ($respHeaders as $hk => $hv) {
                if (strtolower((string) $hk) === 'retry-after') {
                    if (is_array($hv)) {
                        $hv = reset($hv);
                    }
                    $retryAfter = (int) $hv;
                    break;
                }
            }
        }

        $json = json_decode($body, true);
        if (!is_array($json)) {
            $err = 'Invalid JSON response from OpenAI.';
            if ($code >= 400) {
                $err .= ' HTTP ' . $code;
            }
            return [
                'ok' => false,
                'text' => '',
                'usage' => ['input' => 0, 'output' => 0, 'total' => 0],
                'latency_ms' => $latency,
                'error' => $err,
                'http_status' => $code,
                'retry_after' => $retryAfter,
            ];
        }

        if ($code >= 400) {
            $msg = '';
            if (isset($json['error']['message'])) {
                $msg = (string) $json['error']['message'];
            } elseif (isset($json['message'])) {
                $msg = (string) $json['message'];
            }
            if ($msg === '') {
                $msg = 'OpenAI API error (HTTP ' . $code . ').';
            }
            return [
                'ok' => false,
                'text' => '',
                'usage' => ['input' => 0, 'output' => 0, 'total' => 0],
                'latency_ms' => $latency,
                'error' => $msg,
                'http_status' => $code,
                'retry_after' => $retryAfter,
            ];
        }

        $text = $this->extractOutputText($json);

        $usageIn = 0;
        $usageOut = 0;
        $usageTotal = 0;

        if (isset($json['usage']) && is_array($json['usage'])) {
            $usageIn = isset($json['usage']['input_tokens']) ? (int) $json['usage']['input_tokens'] : 0;
            $usageOut = isset($json['usage']['output_tokens']) ? (int) $json['usage']['output_tokens'] : 0;
            $usageTotal = isset($json['usage']['total_tokens']) ? (int) $json['usage']['total_tokens'] : ($usageIn + $usageOut);
        }

        return [
            'ok' => true,
            'text' => $text,
            'usage' => ['input' => $usageIn, 'output' => $usageOut, 'total' => $usageTotal],
            'latency_ms' => $latency,
            'error' => '',
            'http_status' => $code,
            'retry_after' => $retryAfter,
        ];
    }

    /**
     * @param array<string, mixed> $json
     */
    private function extractOutputText(array $json): string
    {
        $parts = [];

        if (isset($json['output']) && is_array($json['output'])) {
            foreach ($json['output'] as $item) {
                if (!is_array($item)) {
                    continue;
                }
                // Some responses include: {type:"message", content:[{type:"output_text", text:"..."}]}
                if (isset($item['content']) && is_array($item['content'])) {
                    foreach ($item['content'] as $c) {
                        if (!is_array($c)) {
                            continue;
                        }
                        if (($c['type'] ?? '') === 'output_text' && isset($c['text'])) {
                            $parts[] = (string) $c['text'];
                        }
                        if (($c['type'] ?? '') === 'text' && isset($c['text'])) {
                            $parts[] = (string) $c['text'];
                        }
                    }
                }
                // Fallbacks.
                if (isset($item['text']) && is_string($item['text'])) {
                    $parts[] = $item['text'];
                }
            }
        }

        // Additional fallback keys sometimes appear.
        if (!$parts) {
            if (isset($json['output_text']) && is_string($json['output_text'])) {
                $parts[] = $json['output_text'];
            }
            if (isset($json['text']) && is_string($json['text'])) {
                $parts[] = $json['text'];
            }
        }

        $text = trim(implode("\n", $parts));
        // Keep it single-line for titles/descriptions.
        $text = preg_replace('/\s+/u', ' ', $text) ?? $text;

        return trim($text);
    }
}
