<?php

namespace WPSN\Modules\AI;

use WPSN\Core\Http\HttpClient;
use WPSN\Core\Logger;
use WPSN\Core\Settings;
use WPSN\Modules\SaaSSync\SaaSSyncService;

defined('ABSPATH') || exit;

/**
 * SaaS AI proxy client.
 *
 * This client routes AI generation through your SaaS backend. The backend can:
 * - proxy to OpenAI/other LLM providers,
 * - enforce plan restrictions,
 * - bill credits,
 * - return a unified response format,
 * - optionally accept the same signed request contract used by licensing/update flows.
 */
final class SaaSAIProxyClient
{
    private SaaSSyncService $saas;
    private Logger $logger;
    private Settings $settings;
    private SaaSAIRequestContract $contract;

    public function __construct(SaaSSyncService $saas, Logger $logger, Settings $settings)
    {
        $this->saas = $saas;
        $this->logger = $logger;
        $this->settings = $settings;
        $this->contract = new SaaSAIRequestContract($settings, $saas);
    }

    /**
     * @param string|array<int, array<string,mixed>>|array<string,mixed> $input
     * @param array<string,mixed> $meta
     * @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,fields?:array<string,string>,credits?:array<string,mixed>,cost_usd?:float}
     */
    public function generate(string $model, $input, int $maxOutputTokens, float $temperature, string $purpose = '', array $meta = []): array
    {
        if (is_array($input) && $this->looksLikeStructuredPayload($input)) {
            return $this->generateStructured($model, $input, $maxOutputTokens, $temperature, $purpose, $meta);
        }

        return $this->dispatch(
            $model,
            is_string($input) ? $input : $this->normalizeText(wp_json_encode($input)),
            $this->wrapLegacyInput(is_string($input) ? $input : $this->normalizeText(wp_json_encode($input)), $purpose, $meta),
            $maxOutputTokens,
            $temperature,
            $purpose,
            $meta
        );
    }

    /**
     * Convenience wrapper for plain-text prompts.
     *
     * @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,fields?:array<string,string>,credits?:array<string,mixed>,cost_usd?:float}
     */
    public function generateText(string $model, string $input, int $maxOutputTokens, float $temperature, string $purpose = '', array $meta = []): array
    {
        return $this->dispatch(
            $model,
            $input,
            $this->wrapLegacyInput($input, $purpose, $meta),
            $maxOutputTokens,
            $temperature,
            $purpose,
            $meta
        );
    }

    /**
     * Structured request wrapper for the server-side AI hub.
     *
     * @param array<string,mixed> $request
     * @param array<string,mixed> $meta
     * @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,fields?:array<string,string>,credits?:array<string,mixed>,cost_usd?:float}
     */
    public function generateStructured(string $model, array $request, int $maxOutputTokens, float $temperature, string $purpose = '', array $meta = []): array
    {
        $structured = $this->normalizeStructuredRequest($request, $purpose, $meta);
        $legacyInput = $this->structuredLegacyPrompt($structured);

        return $this->dispatch(
            $model,
            $legacyInput,
            $structured,
            $maxOutputTokens,
            $temperature,
            $purpose !== '' ? $purpose : (string) ($structured['purpose'] ?? ''),
            $meta
        );
    }

    /**
     * @param array<string,mixed> $structured
     * @param array<string,mixed> $meta
     * @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,fields?:array<string,string>,credits?:array<string,mixed>,cost_usd?:float}
     */
    private function dispatch(string $model, string $legacyInput, array $structured, int $maxOutputTokens, float $temperature, string $purpose = '', array $meta = []): array
    {
        $t0 = microtime(true);

        $token = $this->contract->authToken();
        $licenseKey = $this->contract->licenseKey();
        if ($token === '' && $licenseKey === '') {
            return [
                'ok' => false,
                'text' => '',
                'usage' => ['input' => 0, 'output' => 0, 'total' => 0],
                'latency_ms' => 0,
                'error' => 'SaaS auth token is missing.',
                'http_status' => 401,
                'retry_after' => 0,
            ];
        }

        $endpoint = $this->saas->endpoint(
            'ai_generate',
            (string) $this->settings->get('saas_sync.ai_path', '/wp-seo-next/ai/generate')
        );

        $timeout = (int) $this->settings->get('saas_sync.ai_timeout', 60);
        $timeout = max(3, min(120, $timeout));

        $payload = $this->contract->buildPayload('ai_generate', [
            'model' => $model,
            'input' => $legacyInput,
            'max_output_tokens' => max(16, (int) $maxOutputTokens),
            'temperature' => max(0.0, min(2.0, (float) $temperature)),
            'purpose' => sanitize_key($purpose !== '' ? $purpose : (string) ($structured['purpose'] ?? '')),
            'meta' => $meta,
            'structured' => $structured,
            'input_mode' => 'structured',
            'client' => [
                'plugin' => 'wp-seo-next',
                'version' => defined('WPSN_VERSION') ? (string) WPSN_VERSION : 'dev',
                'wp' => function_exists('get_bloginfo') ? (string) get_bloginfo('version') : '',
            ],
        ]);

        $headers = $this->contract->buildHeaders('ai_generate', $payload);

        $resp = HttpClient::post($endpoint, [
            'headers' => $headers,
            'user-agent' => 'WPSN/' . (defined('WPSN_VERSION') ? WPSN_VERSION : 'dev'),
            'timeout' => $timeout,
            'body' => $this->contract->jsonEncode($payload),
        ], [
            'service' => 'saas_ai_proxy',
            'context' => 'ai_generate',
            'retries' => 1,
            'retry_statuses' => [429, 500, 502, 503, 504],
        ]);

        $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 = $this->extractRetryAfter($resp);

        $json = json_decode($body, true);
        if (!is_array($json)) {
            $err = 'Invalid JSON response from SaaS AI proxy.';
            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 ($retryAfter <= 0 && isset($json['retry_after'])) {
            $retryAfter = (int) $json['retry_after'];
        }

        if ($code >= 400 || (!empty($json['ok']) && $json['ok'] === false)) {
            return [
                'ok' => false,
                'text' => '',
                'usage' => ['input' => 0, 'output' => 0, 'total' => 0],
                'latency_ms' => $latency,
                'error' => $this->extractError($json, $code),
                'http_status' => $code,
                'retry_after' => $retryAfter,
            ];
        }

        $root = $this->responseRoot($json);
        $usage = $this->extractUsage($root);
        $fields = $this->extractFields($root);
        $credits = $this->extractCredits($root, $json);
        $text = $this->extractText($json, $purpose, $meta);

        $out = [
            'ok' => true,
            'text' => $text,
            'usage' => $usage,
            'latency_ms' => $latency,
            'error' => '',
            'http_status' => $code,
            'retry_after' => $retryAfter,
        ];

        if (!empty($fields)) {
            $out['fields'] = $fields;
        }

        if (!empty($credits)) {
            $out['credits'] = $credits;
        }

        if (isset($root['cost_usd']) || isset($json['cost_usd'])) {
            $out['cost_usd'] = isset($root['cost_usd']) ? (float) $root['cost_usd'] : (float) $json['cost_usd'];
        }

        return $out;
    }

    /**
     * @param array<string,mixed> $json
     * @return array<string,mixed>
     */
    private function responseRoot(array $json): array
    {
        foreach (['ai', 'response', 'result', 'payload'] as $key) {
            if (isset($json[$key]) && is_array($json[$key])) {
                return $json[$key];
            }
        }

        return $json;
    }

    /**
     * @param array<string,mixed> $json
     * @param array<string,mixed> $meta
     */
    private function extractText(array $json, string $purpose = '', array $meta = []): string
    {
        $root = $this->responseRoot($json);

        foreach ([$json, $root] as $source) {
            if (isset($source['text']) && is_string($source['text'])) {
                return $this->normalizeText($source['text']);
            }
            if (isset($source['output_text']) && is_string($source['output_text'])) {
                return $this->normalizeText($source['output_text']);
            }
        }

        $field = $this->requestedField($purpose, $meta, $root);
        $fields = $this->extractFields($root);
        if ($field !== '' && isset($fields[$field]) && is_string($fields[$field])) {
            return $this->normalizeText($fields[$field]);
        }

        $parts = [];
        foreach ([$json, $root] as $source) {
            if (isset($source['output']) && is_array($source['output'])) {
                foreach ($source['output'] as $item) {
                    if (!is_array($item)) {
                        continue;
                    }
                    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'];
                            }
                        }
                    }
                    if (isset($item['text']) && is_string($item['text'])) {
                        $parts[] = $item['text'];
                    }
                }
            }
        }

        return $this->normalizeText(implode("\n", $parts));
    }

    /**
     * @param array<string,mixed> $source
     * @return array{input:int,output:int,total:int}
     */
    private function extractUsage(array $source): array
    {
        $usageIn = 0;
        $usageOut = 0;
        $usageTotal = 0;

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

        return ['input' => $usageIn, 'output' => $usageOut, 'total' => $usageTotal];
    }

    /**
     * @param array<string,mixed> $source
     * @return array<string,string>
     */
    private function extractFields(array $source): array
    {
        $fields = [];
        if (!isset($source['fields']) || !is_array($source['fields'])) {
            return $fields;
        }

        foreach ($source['fields'] as $key => $value) {
            if (!is_string($key) || !is_scalar($value)) {
                continue;
            }
            $normalizedKey = sanitize_key($key);
            if ($normalizedKey === '') {
                continue;
            }
            $text = $this->normalizeText((string) $value);
            if ($text !== '') {
                $fields[$normalizedKey] = $text;
            }
        }

        return $fields;
    }

    /**
     * @param array<string,mixed> $root
     * @param array<string,mixed> $json
     * @return array<string,mixed>
     */
    private function extractCredits(array $root, array $json): array
    {
        if (isset($root['credits']) && is_array($root['credits'])) {
            return $root['credits'];
        }
        if (isset($json['credits']) && is_array($json['credits'])) {
            return $json['credits'];
        }
        return [];
    }

    /**
     * @param array<string,mixed> $json
     */
    private function extractError(array $json, int $code): string
    {
        $root = $this->responseRoot($json);
        foreach ([$json, $root] as $source) {
            if (isset($source['error']) && is_string($source['error']) && trim($source['error']) !== '') {
                return trim((string) $source['error']);
            }
            if (isset($source['message']) && is_string($source['message']) && trim($source['message']) !== '') {
                return trim((string) $source['message']);
            }
            if (isset($source['error']['message']) && is_string($source['error']['message'])) {
                return trim((string) $source['error']['message']);
            }
        }

        return 'SaaS AI proxy error (HTTP ' . $code . ').';
    }

    /**
     * @param array<string,mixed> $source
     * @param array<string,mixed> $meta
     */
    private function requestedField(string $purpose, array $meta, array $source = []): string
    {
        foreach ([
            $meta['field'] ?? null,
            $source['field'] ?? null,
            (isset($source['task']) && is_array($source['task'])) ? ($source['task']['field'] ?? null) : null,
        ] as $candidate) {
            if (!is_scalar($candidate)) {
                continue;
            }
            $field = sanitize_key((string) $candidate);
            if (in_array($field, ['title', 'description'], true)) {
                return $field;
            }
        }

        $purpose = sanitize_key($purpose);
        if ($purpose !== '') {
            if (strpos($purpose, 'title') !== false) {
                return 'title';
            }
            if (strpos($purpose, 'description') !== false || strpos($purpose, 'desc') !== false) {
                return 'description';
            }
        }

        return '';
    }

    /**
     * @param array<string,mixed> $input
     */
    private function looksLikeStructuredPayload(array $input): bool
    {
        foreach (['task', 'inputs', 'context', 'constraints', 'prompt', 'legacy_prompt', 'input_text', 'desired_output'] as $key) {
            if (array_key_exists($key, $input)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<string,mixed> $request
     * @param array<string,mixed> $meta
     * @return array<string,mixed>
     */
    private function normalizeStructuredRequest(array $request, string $purpose = '', array $meta = []): array
    {
        $structured = $request;
        $structured['purpose'] = sanitize_key((string) ($structured['purpose'] ?? $purpose));

        foreach (['task', 'inputs', 'context', 'constraints', 'meta', 'prompt'] as $key) {
            if (!isset($structured[$key]) || !is_array($structured[$key])) {
                $structured[$key] = [];
            }
        }

        if (!empty($meta)) {
            $structured['meta'] = array_replace((array) $structured['meta'], $meta);
        }

        $legacyPrompt = '';
        if (isset($structured['legacy_prompt']) && is_string($structured['legacy_prompt'])) {
            $legacyPrompt = $structured['legacy_prompt'];
        }
        if ($legacyPrompt === '' && isset($structured['prompt']['text']) && is_string($structured['prompt']['text'])) {
            $legacyPrompt = (string) $structured['prompt']['text'];
        }
        if ($legacyPrompt === '' && isset($structured['input_text']) && is_string($structured['input_text'])) {
            $legacyPrompt = (string) $structured['input_text'];
        }
        $structured['legacy_prompt'] = $legacyPrompt;
        $structured['input_text'] = $legacyPrompt;

        return $structured;
    }

    /**
     * @param array<string,mixed> $meta
     * @return array<string,mixed>
     */
    private function wrapLegacyInput(string $input, string $purpose = '', array $meta = []): array
    {
        $field = '';
        if (!empty($meta['field']) && is_scalar($meta['field'])) {
            $field = sanitize_key((string) $meta['field']);
        }

        return [
            'purpose' => sanitize_key($purpose),
            'task' => [
                'type' => 'legacy_prompt',
                'field' => $field,
            ],
            'inputs' => [],
            'context' => [],
            'constraints' => [],
            'meta' => $meta,
            'prompt' => [
                'text' => $input,
            ],
            'legacy_prompt' => $input,
            'input_text' => $input,
        ];
    }

    /**
     * @param array<string,mixed> $structured
     */
    private function structuredLegacyPrompt(array $structured): string
    {
        foreach ([
            $structured['legacy_prompt'] ?? null,
            (isset($structured['prompt']) && is_array($structured['prompt'])) ? ($structured['prompt']['text'] ?? null) : null,
            $structured['input_text'] ?? null,
            $structured['input'] ?? null,
        ] as $candidate) {
            if (is_string($candidate) && trim($candidate) !== '') {
                return $candidate;
            }
        }

        if (!empty($structured['context']) && is_array($structured['context'])) {
            foreach (['content', 'excerpt', 'post_title', 'title'] as $field) {
                if (!empty($structured['context'][$field]) && is_scalar($structured['context'][$field])) {
                    return $this->normalizeText((string) $structured['context'][$field]);
                }
            }
        }

        return '';
    }

    private function normalizeText($text): string
    {
        if (!is_scalar($text)) {
            return '';
        }
        $text = (string) $text;
        $text = preg_replace('/\s+/u', ' ', $text) ?? $text;
        return trim($text);
    }

    /**
     * @param array<string,mixed> $resp
     */
    private function extractRetryAfter(array $resp): int
    {
        $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;
                }
            }
        }

        return $retryAfter;
    }
}
