<?php

namespace VulnTitan\Api;

/**
 * VulnTitan API Client
 *
 * This class communicates with the external VulnTitan API (https://vulntitan.com/api/vulnerabilities)
 * to retrieve known vulnerabilities for plugins, themes, and WordPress core.
 *
 * Data sent:
 * - Plugin/theme/core slug and version.
 *
 * No personal or user-identifiable data is transmitted.
 *
 * @package VulnTitan
 */
class Client {

    protected const CACHE_TTL = 12 * HOUR_IN_SECONDS;
    protected const DEFAULT_TIMEOUT = 15;
    protected const MAX_RETRIES = 2;
    protected const RETRY_STATUS_CODES = [429, 502, 503, 504];

    protected string $endpoint = 'https://vulntitan.com/api/vulnerabilities';

    public function checkPlugin(string $slug, string $version): array
    {
        $url = esc_url_raw(add_query_arg([
            'type' => 'plugin',
            'slug' => $slug,
            'version' => $version,
        ], $this->endpoint));

        return $this->fetch($url);
    }

    public function checkTheme(string $slug, string $version): array
    {
        $url = esc_url_raw(add_query_arg([
            'type' => 'theme',
            'slug' => $slug,
            'version' => $version,
        ], $this->endpoint));

        return $this->fetch($url);
    }

    public function checkCore(string $version): array
    {
        $url = esc_url_raw(add_query_arg([
            'type' => 'core',
            'version' => $version,
        ], $this->endpoint));

        return $this->fetch($url);
    }

    /**
     * Executes a GET request to the external VulnTitan vulnerability API.
     *
     * Sends minimal data (slug and version) to retrieve vulnerability information.
     * Includes site URL in the User-Agent header for tracking plugin usage (non-personal).
     *
     * @param string $url The fully constructed API URL with query parameters.
     * @return array Structured response including success, message, and vulnerabilities.
     */
    protected function fetch(string $url): array
    {
        $cacheKey = $this->getCacheKey($url);
        $cachedResult = $this->getCachedResult($cacheKey);
        if ($cachedResult !== null) {
            return $cachedResult;
        }

        $attempts = self::MAX_RETRIES + 1;
        $attempt = 0;
        $lastMessage = '';

        for ($attempt = 1; $attempt <= $attempts; $attempt++) {
            $response = wp_remote_get($url, [
                'headers' => [
                    'User-Agent' => 'VulnTitanScanner/1.0; ' . home_url(),
                ],
                'timeout' => self::DEFAULT_TIMEOUT,
            ]);

            if (is_wp_error($response)) {
                $lastMessage = sprintf(
                    // Translators: %s is the error message returned by the WordPress HTTP API.
                    esc_html__('Request failed: %s', 'vulntitan'),
                    $response->get_error_message()
                );

                if ($this->shouldRetry(null, true, $attempt, $attempts)) {
                    $this->applyRetryDelay($attempt);
                    continue;
                }

                break;
            }

            $status = wp_remote_retrieve_response_code($response);
            $body = wp_remote_retrieve_body($response);

            if ($status !== 200) {
                $lastMessage = $this->buildStatusMessage($status);

                if ($this->shouldRetry($status, false, $attempt, $attempts)) {
                    $this->applyRetryDelay($attempt);
                    continue;
                }

                break;
            }

            $data = json_decode($body, true);

            if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
                $lastMessage = esc_html__('Invalid JSON response or missing data key.', 'vulntitan');

                if ($this->shouldRetry(null, false, $attempt, $attempts)) {
                    $this->applyRetryDelay($attempt);
                    continue;
                }

                break;
            }

            $vulnerabilities = is_array($data['data'] ?? null) ? array_values($data['data']) : [];
            $this->storeCache($cacheKey, $vulnerabilities);

            return [
                'success' => true,
                'message' => empty($vulnerabilities)
                    ? esc_html__('No vulnerabilities found.', 'vulntitan')
                    : esc_html__('Vulnerabilities found.', 'vulntitan'),
                'vulnerabilities' => $vulnerabilities,
                'meta' => [
                    'cached' => false,
                    'cache_age' => 0,
                ],
            ];
        }

        return [
            'success' => false,
            'message' => $lastMessage !== '' ? $lastMessage : esc_html__('Unknown error occurred.', 'vulntitan'),
            'vulnerabilities' => [],
            'meta' => [
                'cached' => false,
                'cache_age' => 0,
            ],
        ];
    }

    protected function getCacheKey(string $url): string
    {
        return 'vulntitan_vuln_' . md5($url);
    }

    protected function getCachedResult(string $cacheKey): ?array
    {
        $cached = get_transient($cacheKey);
        if (!is_array($cached) || !isset($cached['cached_at'])) {
            return null;
        }

        $vulnerabilities = is_array($cached['vulnerabilities'] ?? null) ? $cached['vulnerabilities'] : [];
        $cacheAge = max(0, time() - (int) $cached['cached_at']);

        return [
            'success' => true,
            'message' => empty($vulnerabilities)
                ? esc_html__('No vulnerabilities found.', 'vulntitan')
                : esc_html__('Vulnerabilities found.', 'vulntitan'),
            'vulnerabilities' => $vulnerabilities,
            'meta' => [
                'cached' => true,
                'cache_age' => $cacheAge,
            ],
        ];
    }

    protected function storeCache(string $cacheKey, array $vulnerabilities): void
    {
        set_transient(
            $cacheKey,
            [
                'cached_at' => time(),
                'vulnerabilities' => $vulnerabilities,
            ],
            self::CACHE_TTL
        );
    }

    protected function buildStatusMessage(int $status): string
    {
        if ($status === 429) {
            return esc_html__('Rate limit reached. Please retry in a few minutes.', 'vulntitan');
        }

        if (in_array($status, [502, 503, 504], true)) {
            return sprintf(
                // Translators: %d is the HTTP status code returned by the API (e.g., 502, 503, 504).
                esc_html__('VulnTitan API is temporarily unavailable (HTTP %d). Please retry.', 'vulntitan'),
                $status
            );
        }

        return sprintf(
            // Translators: %d is the HTTP status code returned by the API (e.g., 403, 500).
            esc_html__('API returned HTTP status %d.', 'vulntitan'),
            $status
        );
    }

    protected function shouldRetry(?int $status, bool $isError, int $attempt, int $attempts): bool
    {
        if ($attempt >= $attempts) {
            return false;
        }

        if ($isError) {
            return true;
        }

        if ($status === null) {
            return true;
        }

        return in_array($status, self::RETRY_STATUS_CODES, true);
    }

    protected function applyRetryDelay(int $attempt): void
    {
        $delayMs = min(2000, 300 * $attempt);
        usleep($delayMs * 1000);
    }
}
