<?php

declare(strict_types=1);

namespace SeoScope\Services;

use JsonException;
use RuntimeException;
use SeoScope\i18n\SupportedLocales;

/**
 * Calls OpenAI or Anthropic chat APIs and returns validated structured SEO insight fields.
 *
 * @phpstan-type SettingsSnapshot array{
 *   enabled: bool,
 *   provider: string,
 *   openai_model: string,
 *   anthropic_model: string,
 *   openai_api_key: ?string,
 *   anthropic_api_key: ?string,
 * }
 */
final class SeoAiCompletionService
{
    private const OPENAI_CHAT_URL = 'https://api.openai.com/v1/chat/completions';

    private const ANTHROPIC_MESSAGES_URL = 'https://api.anthropic.com/v1/messages';

    private const ANTHROPIC_VERSION = '2023-06-01';

    private const CURL_TIMEOUT_SEC = 55;

    private const CURL_CONNECT_TIMEOUT_SEC = 12;

    /**
     * @param AiSettingsService|\Closure(): SettingsSnapshot $settings
     * @param null|\Closure(string $method, string $url, list<string> $headers, ?string $body): array{status:int, body:string} $transport
     */
    public function __construct(
        private AiSettingsService|\Closure $settings,
        private ?\Closure $transport = null,
    ) {}

    /**
     * @param array<string, mixed> $assistantPayload Payload from {@see SeoAiAssistantPayloadService::build()} (`payload` key)
     * @param array<string, mixed> $overrides Optional: ai_provider, openai_api_key, anthropic_api_key, openai_model, anthropic_model
     *
     * @return array{
     *   ok: bool,
     *   executive_summary?: string,
     *   seo_interpretation?: string,
     *   priority_recommendation?: string,
     *   prompt_version?: int,
     *   provider?: string,
     *   model?: string,
     *   usage?: array{provider:string, input_tokens:int, output_tokens:int, total_tokens:int},
     *   error_code?: string,
     *   message?: string,
     * }
     */
    public function completeStructuredInsight(array $assistantPayload, string $uiLocale, array $overrides = []): array
    {
        if (!extension_loaded('curl') && $this->transport === null) {
            return [
                'ok' => false,
                'error_code' => 'no_curl',
                'message' => __('seo_ai.completion.no_curl'),
            ];
        }

        $cfg = $this->effectiveConfig($overrides);
        if (!$cfg['enabled']) {
            return [
                'ok' => false,
                'error_code' => 'not_enabled',
                'message' => __('seo_ai.completion.not_enabled'),
            ];
        }

        $provider = $cfg['provider'];
        $apiKey = $provider === AiSettingsService::PROVIDER_ANTHROPIC
            ? $cfg['anthropic_api_key']
            : $cfg['openai_api_key'];
        if ($apiKey === null || $apiKey === '') {
            return [
                'ok' => false,
                'error_code' => 'missing_key',
                'message' => __('seo_ai.completion.missing_key'),
            ];
        }

        $model = $provider === AiSettingsService::PROVIDER_ANTHROPIC
            ? $cfg['anthropic_model']
            : $cfg['openai_model'];

        $locale = SupportedLocales::normalize($uiLocale);
        $label = SupportedLocales::label($locale);

        $systemOverride = $overrides['system_prompt'] ?? null;
        $system = is_string($systemOverride) && trim($systemOverride) !== ''
            ? trim($systemOverride)
            : SeoAiAssistantPrompts::systemPrompt($locale, $label);

        $userJsonOverride = $overrides['user_json'] ?? null;
        $userJson = is_string($userJsonOverride) && trim($userJsonOverride) !== ''
            ? trim($userJsonOverride)
            : SeoAiAssistantPrompts::userEnvelope($assistantPayload);

        $promptVersion = isset($overrides['prompt_version']) && is_numeric($overrides['prompt_version'])
            ? (int) $overrides['prompt_version']
            : SeoAiAssistantPrompts::PROMPT_VERSION;

        try {
            if ($provider === AiSettingsService::PROVIDER_ANTHROPIC) {
                $llm = $this->callAnthropic($apiKey, $model, $system, $userJson);
            } else {
                $llm = $this->callOpenAi($apiKey, $model, $system, $userJson);
            }
        } catch (SeoAiProviderException $e) {
            return $this->mapProviderExceptionToResult($e, $provider, $model);
        } catch (\Throwable $e) {
            return [
                'ok' => false,
                'error_code' => 'transport',
                'message' => __('seo_ai.completion.transport', [
                    'detail' => self::sanitizeTransportDetail($e->getMessage()),
                ]),
                'provider' => $provider,
                'model' => $model,
            ];
        }

        $rawText = $llm['text'];
        if ($rawText === '') {
            return [
                'ok' => false,
                'error_code' => 'empty_response',
                'message' => __('seo_ai.completion.empty_response'),
                'provider' => $provider,
                'model' => $model,
            ];
        }

        $parsed = SeoAiStructuredInsightParser::parse($rawText);
        if (!$parsed['ok']) {
            return [
                'ok' => false,
                'error_code' => 'parse_' . ($parsed['error'] ?? 'unknown'),
                'message' => __('seo_ai.completion.parse_failed'),
                'provider' => $provider,
                'model' => $model,
            ];
        }

        return [
            'ok' => true,
            'executive_summary' => $parsed['executive_summary'],
            'seo_interpretation' => $parsed['seo_interpretation'],
            'priority_recommendation' => $parsed['priority_recommendation'],
            'prompt_version' => $promptVersion,
            'provider' => $provider,
            'model' => $model,
            'usage' => [
                'provider' => $provider,
                'input_tokens' => (int) ($llm['input_tokens'] ?? 0),
                'output_tokens' => (int) ($llm['output_tokens'] ?? 0),
                'total_tokens' => (int) ($llm['total_tokens'] ?? 0),
            ],
        ];
    }

    /**
     * @return array{
     *   ok: false,
     *   error_code: string,
     *   message: string,
     *   provider: string,
     *   model: string,
     * }
     */
    private function mapProviderExceptionToResult(SeoAiProviderException $e, string $provider, string $model): array
    {
        $code = $e->publicErrorCode;
        $base = [
            'ok' => false,
            'error_code' => $code,
            'provider' => $provider,
            'model' => $model,
        ];

        return match ($code) {
            'auth_failed' => [...$base, 'message' => __('seo_ai.completion.auth_failed')],
            'provider_rate_limited' => [...$base, 'message' => __('seo_ai.completion.provider_rate_limited')],
            'provider_unavailable' => [...$base, 'message' => __('seo_ai.completion.provider_unavailable')],
            'provider_timeout' => [...$base, 'message' => __('seo_ai.completion.provider_timeout')],
            'invalid_upstream_json' => [...$base, 'message' => __('seo_ai.completion.invalid_upstream_json')],
            'provider_http' => [...$base, 'message' => __('seo_ai.completion.provider_http', [
                'status' => $e->httpStatus >= 100 && $e->httpStatus <= 599 ? $e->httpStatus : 502,
            ])],
            'transport' => [...$base, 'message' => __('seo_ai.completion.transport', [
                'detail' => $e->getMessage() !== '' ? $e->getMessage() : __('seo_ai.completion.transport_detail_fallback'),
            ])],
            default => [
                ...$base,
                'error_code' => 'transport',
                'message' => __('seo_ai.completion.transport', ['detail' => self::sanitizeTransportDetail($code)]),
            ],
        };
    }

    private static function sanitizeTransportDetail(string $raw): string
    {
        $s = trim((string) preg_replace('/\s+/u', ' ', $raw));
        // Avoid leaking absolute paths from some PHP/cURL messages.
        $s = (string) preg_replace('#(/[^\s:]+/)+[^\s:]+#', '<path>', $s);
        if (mb_strlen($s) > 160) {
            $s = mb_substr($s, 0, 157) . '…';
        }

        return $s !== '' ? $s : 'temporary error';
    }

    /**
     * @return SettingsSnapshot
     */
    private function snapshotSettings(): array
    {
        if ($this->settings instanceof AiSettingsService) {
            $s = $this->settings;

            return [
                'enabled' => $s->isEnabled(),
                'provider' => $s->provider(),
                'openai_model' => $s->openaiModel(),
                'anthropic_model' => $s->anthropicModel(),
                'openai_api_key' => $s->apiKeyForProvider(AiSettingsService::PROVIDER_OPENAI),
                'anthropic_api_key' => $s->apiKeyForProvider(AiSettingsService::PROVIDER_ANTHROPIC),
            ];
        }

        $snap = ($this->settings)();
        if (!is_array($snap)) {
            throw new RuntimeException('Invalid AI settings snapshot.');
        }

        return [
            'enabled' => !empty($snap['enabled']),
            'provider' => $this->normalizeProvider((string) ($snap['provider'] ?? AiSettingsService::PROVIDER_OPENAI)),
            'openai_model' => trim((string) ($snap['openai_model'] ?? '')) !== '' ? trim((string) $snap['openai_model']) : AiSettingsService::DEFAULT_OPENAI_MODEL,
            'anthropic_model' => trim((string) ($snap['anthropic_model'] ?? '')) !== '' ? trim((string) $snap['anthropic_model']) : AiSettingsService::DEFAULT_ANTHROPIC_MODEL,
            'openai_api_key' => isset($snap['openai_api_key']) && is_string($snap['openai_api_key']) && $snap['openai_api_key'] !== '' ? $snap['openai_api_key'] : null,
            'anthropic_api_key' => isset($snap['anthropic_api_key']) && is_string($snap['anthropic_api_key']) && $snap['anthropic_api_key'] !== '' ? $snap['anthropic_api_key'] : null,
        ];
    }

    /**
     * @param array<string, mixed> $overrides
     *
     * @return SettingsSnapshot&array{provider:string}
     */
    private function effectiveConfig(array $overrides): array
    {
        $base = $this->snapshotSettings();
        $provider = $this->normalizeProvider((string) ($overrides['ai_provider'] ?? $base['provider']));
        $base['provider'] = $provider;

        $om = trim((string) ($overrides['openai_model'] ?? ''));
        if ($om !== '') {
            $base['openai_model'] = $om;
        }
        $am = trim((string) ($overrides['anthropic_model'] ?? ''));
        if ($am !== '') {
            $base['anthropic_model'] = $am;
        }

        $ok = trim((string) ($overrides['openai_api_key'] ?? ''));
        if ($ok !== '' && $ok !== AiSettingsService::MASKED_SECRET) {
            $base['openai_api_key'] = $ok;
        }
        $ak = trim((string) ($overrides['anthropic_api_key'] ?? ''));
        if ($ak !== '' && $ak !== AiSettingsService::MASKED_SECRET) {
            $base['anthropic_api_key'] = $ak;
        }

        return $base;
    }

    private function normalizeProvider(string $raw): string
    {
        if ($this->settings instanceof AiSettingsService) {
            return $this->settings->normalizeProviderPublic($raw);
        }
        $raw = strtolower(trim($raw));

        return in_array($raw, AiSettingsService::PROVIDERS, true) ? $raw : AiSettingsService::PROVIDER_OPENAI;
    }

    /**
     * @return array{text:string, input_tokens:int, output_tokens:int, total_tokens:int}
     */
    private function callOpenAi(string $apiKey, string $model, string $system, string $userJson): array
    {
        $body = [
            'model' => $model,
            'temperature' => 0.25,
            'max_tokens' => 1200,
            'response_format' => ['type' => 'json_object'],
            'messages' => [
                ['role' => 'system', 'content' => $system],
                ['role' => 'user', 'content' => $userJson],
            ],
        ];
        $json = json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);

        $res = $this->request(
            'POST',
            self::OPENAI_CHAT_URL,
            [
                'Authorization: Bearer ' . $apiKey,
                'Content-Type: application/json',
            ],
            $json
        );

        if ($res['status'] === 401 || $res['status'] === 403) {
            throw new SeoAiProviderException('auth_failed');
        }
        if ($res['status'] < 200 || $res['status'] >= 300) {
            $this->throwForHttpStatus($res['status'], $res['body']);
        }

        try {
            $decoded = json_decode($res['body'], true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException) {
            throw new SeoAiProviderException('invalid_upstream_json');
        }
        if (!is_array($decoded)) {
            throw new SeoAiProviderException('invalid_upstream_json');
        }
        $content = $decoded['choices'][0]['message']['content'] ?? null;
        $text = is_string($content) ? trim($content) : '';
        $u = $decoded['usage'] ?? null;
        $prompt = is_array($u) ? (int) ($u['prompt_tokens'] ?? 0) : 0;
        $comp = is_array($u) ? (int) ($u['completion_tokens'] ?? 0) : 0;
        $total = is_array($u) ? (int) ($u['total_tokens'] ?? 0) : 0;
        if ($total <= 0) {
            $total = $prompt + $comp;
        }

        return [
            'text' => $text,
            'input_tokens' => $prompt,
            'output_tokens' => $comp,
            'total_tokens' => $total,
        ];
    }

    /**
     * @return array{text:string, input_tokens:int, output_tokens:int, total_tokens:int}
     */
    private function callAnthropic(string $apiKey, string $model, string $system, string $userJson): array
    {
        $body = [
            'model' => $model,
            'max_tokens' => 1200,
            'temperature' => 0.25,
            'system' => $system,
            'messages' => [
                ['role' => 'user', 'content' => $userJson],
            ],
        ];
        $json = json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);

        $res = $this->request(
            'POST',
            self::ANTHROPIC_MESSAGES_URL,
            [
                'x-api-key: ' . $apiKey,
                'anthropic-version: ' . self::ANTHROPIC_VERSION,
                'Content-Type: application/json',
            ],
            $json
        );

        if ($res['status'] === 401 || $res['status'] === 403) {
            throw new SeoAiProviderException('auth_failed');
        }
        if ($res['status'] < 200 || $res['status'] >= 300) {
            $this->throwForHttpStatus($res['status'], $res['body']);
        }

        try {
            $decoded = json_decode($res['body'], true, 512, JSON_THROW_ON_ERROR);
        } catch (JsonException) {
            throw new SeoAiProviderException('invalid_upstream_json');
        }
        if (!is_array($decoded)) {
            throw new SeoAiProviderException('invalid_upstream_json');
        }
        $parts = $decoded['content'] ?? [];
        if (!is_array($parts)) {
            return [
                'text' => '',
                'input_tokens' => 0,
                'output_tokens' => 0,
                'total_tokens' => 0,
            ];
        }
        $text = '';
        foreach ($parts as $block) {
            if (!is_array($block)) {
                continue;
            }
            if (($block['type'] ?? '') === 'text' && isset($block['text']) && is_string($block['text'])) {
                $text .= $block['text'];
            }
        }
        $u = $decoded['usage'] ?? null;
        $inTok = is_array($u) ? (int) ($u['input_tokens'] ?? 0) : 0;
        $outTok = is_array($u) ? (int) ($u['output_tokens'] ?? 0) : 0;
        $total = $inTok + $outTok;

        return [
            'text' => trim($text),
            'input_tokens' => $inTok,
            'output_tokens' => $outTok,
            'total_tokens' => $total,
        ];
    }

    /**
     * @param list<string> $headers
     *
     * @return array{status:int, body:string}
     */
    private function request(string $method, string $url, array $headers, ?string $body): array
    {
        if ($this->transport !== null) {
            return ($this->transport)($method, $url, $headers, $body);
        }

        return $this->curlDefault($method, $url, $headers, $body);
    }

    /**
     * @param list<string> $headers
     *
     * @return array{status:int, body:string}
     */
    private function curlDefault(string $method, string $url, array $headers, ?string $body): array
    {
        $ch = curl_init($url);
        if ($ch === false) {
            throw new SeoAiProviderException('transport', self::sanitizeTransportDetail('cURL init failed'));
        }
        $opts = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => self::CURL_TIMEOUT_SEC,
            CURLOPT_CONNECTTIMEOUT => self::CURL_CONNECT_TIMEOUT_SEC,
        ];
        if (strtoupper($method) === 'POST') {
            $opts[CURLOPT_POST] = true;
            $opts[CURLOPT_POSTFIELDS] = $body ?? '';
        } else {
            $opts[CURLOPT_HTTPGET] = true;
        }
        curl_setopt_array($ch, $opts);
        $responseBody = curl_exec($ch);
        $err = curl_error($ch);
        $errno = (int) curl_errno($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);
        if ($responseBody === false) {
            $errLower = strtolower($err);
            if ($errno === 28 || str_contains($errLower, 'timed out') || str_contains($errLower, 'timeout')) {
                throw new SeoAiProviderException('provider_timeout', $err);
            }
            if (in_array($errno, [6, 7], true)) {
                throw new SeoAiProviderException('provider_unavailable', $err);
            }
            throw new SeoAiProviderException('transport', self::sanitizeTransportDetail($err !== '' ? $err : 'curl_' . $errno));
        }

        return ['status' => $status, 'body' => (string) $responseBody];
    }

    /**
     * @throws SeoAiProviderException
     */
    private function throwForHttpStatus(int $status, string $body): void
    {
        if ($status < 100) {
            throw new SeoAiProviderException('provider_unavailable', $this->shortErrorDetail($body), 0);
        }
        if ($status === 429) {
            throw new SeoAiProviderException('provider_rate_limited', $this->shortErrorDetail($body), $status);
        }
        if (in_array($status, [502, 503, 504], true)) {
            throw new SeoAiProviderException('provider_unavailable', $this->shortErrorDetail($body), $status);
        }
        if ($status === 408) {
            throw new SeoAiProviderException('provider_timeout', $this->shortErrorDetail($body), $status);
        }

        throw new SeoAiProviderException('provider_http', $this->shortErrorDetail($body), $status);
    }

    private function shortErrorDetail(string $body): string
    {
        $body = trim($body);
        if ($body === '') {
            return '';
        }
        $decoded = json_decode($body, true);
        if (is_array($decoded)) {
            $msg = $decoded['error']['message'] ?? $decoded['error']['type'] ?? $decoded['message'] ?? null;
            if (is_string($msg) && $msg !== '') {
                return mb_substr($msg, 0, 200);
            }
        }

        return mb_substr(preg_replace('/\s+/', ' ', $body) ?? $body, 0, 200);
    }
}
