<?php

declare(strict_types=1);

namespace SeoScope\Services;

use RuntimeException;
use SeoScope\Repositories\SettingsRepository;

/**
 * Resolves a short list of recent OpenAI chat models (API + cache, with static fallback).
 */
final class OpenAiModelCatalog
{
    public const CACHE_SETTING = 'ai.openai.models_cache';

    public const CACHE_TTL_SECONDS = 3600;

    public const MAX_MODELS = 10;

    private const MODELS_URL = 'https://api.openai.com/v1/models';

    /** @var list<string> Curated when API is unavailable (newest-first intent). */
    private const FALLBACK_MODELS = [
        'gpt-4.1',
        'gpt-4.1-mini',
        'gpt-4.1-nano',
        'gpt-4o',
        'gpt-4o-mini',
        'o4-mini',
        'o3',
        'o3-mini',
        'gpt-4-turbo',
        'chatgpt-4o-latest',
    ];

    public function __construct(
        private SettingsRepository $repo,
        private AiSettingsService $aiSettings
    ) {}

    /**
     * @return array{models:list<string>,from_api:bool}
     */
    public function choicesForSettings(string $selectedModel = ''): array
    {
        $selectedModel = trim($selectedModel);
        $models = $this->loadCached();
        $fromApi = false;

        if ($models === null) {
            $apiKey = $this->aiSettings->apiKeyForProvider(AiSettingsService::PROVIDER_OPENAI);
            if ($apiKey !== null) {
                $fetched = $this->fetchFromApi($apiKey);
                if ($fetched !== []) {
                    $this->storeCache($fetched);
                    $models = $fetched;
                    $fromApi = true;
                }
            }
        } else {
            $fromApi = true;
        }

        if ($models === null || $models === []) {
            $models = self::FALLBACK_MODELS;
            $fromApi = false;
        }

        $models = $this->ensureSelected($models, $selectedModel);

        return ['models' => $models, 'from_api' => $fromApi];
    }

    /**
     * Force-refresh from OpenAI API (requires stored API key).
     *
     * @return array{ok:bool,message:string,models:list<string>,from_api:bool}
     */
    public function refresh(): array
    {
        if (!extension_loaded('curl')) {
            return [
                'ok' => false,
                'message' => __('settings.ai.test_no_curl'),
                'models' => self::FALLBACK_MODELS,
                'from_api' => false,
            ];
        }

        $apiKey = $this->aiSettings->apiKeyForProvider(AiSettingsService::PROVIDER_OPENAI);
        if ($apiKey === null) {
            return [
                'ok' => false,
                'message' => __('settings.ai.test_missing_key'),
                'models' => self::FALLBACK_MODELS,
                'from_api' => false,
            ];
        }

        $fetched = $this->fetchFromApi($apiKey);
        if ($fetched === []) {
            return [
                'ok' => false,
                'message' => __('settings.ai.openai_models_fetch_failed'),
                'models' => $this->ensureSelected(self::FALLBACK_MODELS, $this->aiSettings->openaiModel()),
                'from_api' => false,
            ];
        }

        $this->storeCache($fetched);
        $selected = $this->aiSettings->openaiModel();

        return [
            'ok' => true,
            'message' => __('settings.ai.openai_models_refreshed'),
            'models' => $this->ensureSelected($fetched, $selected),
            'from_api' => true,
        ];
    }

    /**
     * @return null|list<string>
     */
    private function loadCached(): ?array
    {
        $raw = $this->repo->get(self::CACHE_SETTING);
        if ($raw === null || trim($raw) === '') {
            return null;
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return null;
        }
        $at = isset($decoded['at']) ? strtotime((string) $decoded['at']) : false;
        if ($at === false || (time() - $at) > self::CACHE_TTL_SECONDS) {
            return null;
        }
        $models = $decoded['models'] ?? null;
        if (!is_array($models)) {
            return null;
        }
        $out = [];
        foreach ($models as $m) {
            if (is_string($m) && $m !== '') {
                $out[] = $m;
            }
        }

        return $out !== [] ? array_slice($out, 0, self::MAX_MODELS) : null;
    }

    /**
     * @param list<string> $models
     */
    private function storeCache(array $models): void
    {
        $payload = json_encode([
            'at' => date('c'),
            'models' => array_slice($models, 0, self::MAX_MODELS),
        ], JSON_UNESCAPED_SLASHES);
        $this->repo->set(self::CACHE_SETTING, is_string($payload) ? $payload : null);
    }

    /**
     * @return list<string>
     */
    private function fetchFromApi(string $apiKey): array
    {
        try {
            $res = $this->curlGet(self::MODELS_URL, [
                'Authorization: Bearer ' . $apiKey,
            ]);
        } catch (\Throwable) {
            return [];
        }

        if ($res['status'] < 200 || $res['status'] >= 300) {
            return [];
        }

        $decoded = json_decode($res['body'], true);
        if (!is_array($decoded) || !isset($decoded['data']) || !is_array($decoded['data'])) {
            return [];
        }

        $candidates = [];
        foreach ($decoded['data'] as $row) {
            if (!is_array($row)) {
                continue;
            }
            $id = trim((string) ($row['id'] ?? ''));
            if ($id === '' || !$this->isChatModelId($id)) {
                continue;
            }
            $created = (int) ($row['created'] ?? 0);
            $candidates[] = ['id' => $id, 'created' => $created];
        }

        if ($candidates === []) {
            return [];
        }

        usort($candidates, static fn (array $a, array $b): int => $b['created'] <=> $a['created']);

        $out = [];
        foreach ($candidates as $c) {
            $out[] = $c['id'];
            if (count($out) >= self::MAX_MODELS) {
                break;
            }
        }

        return $out;
    }

    private function isChatModelId(string $id): bool
    {
        $lower = strtolower($id);
        foreach (['embedding', 'whisper', 'tts', 'dall-e', 'davinci', 'babbage', 'moderation', 'transcribe', 'realtime', 'audio', 'search', 'instruct', 'legacy'] as $bad) {
            if (str_contains($lower, $bad)) {
                return false;
            }
        }

        return (bool) preg_match('/^(gpt-|o\d|chatgpt-)/', $lower);
    }

    /**
     * @param list<string> $models
     * @return list<string>
     */
    private function ensureSelected(array $models, string $selected): array
    {
        if ($selected === '') {
            return $models;
        }
        if (in_array($selected, $models, true)) {
            return $models;
        }

        return array_merge([$selected], $models);
    }

    /**
     * @param list<string> $headers
     * @return array{status:int,body:string}
     */
    private function curlGet(string $url, array $headers): array
    {
        $ch = curl_init($url);
        if ($ch === false) {
            throw new RuntimeException('Failed to initialize cURL.');
        }
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPGET => true,
            CURLOPT_HTTPHEADER => $headers,
            CURLOPT_TIMEOUT => 25,
            CURLOPT_CONNECTTIMEOUT => 10,
        ]);
        $body = curl_exec($ch);
        $err = curl_error($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        if ($body === false) {
            throw new RuntimeException($err !== '' ? $err : 'Empty response from OpenAI.');
        }

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