<?php

namespace WPUltimateSecurity\UltimateSecurity\Modules\Monitoring\VulnerabilityScanner;

use WP_Error;

/**
 * Patchstack API Provider
 *
 * Fallback integration with Patchstack Vulnerability Database API
 * for checking plugins, themes, and WordPress core for known vulnerabilities.
 *
 * @see https://patchstack.com/database/api
 */
class PatchstackProvider implements VulnerabilityProviderInterface
{
    /**
     * Patchstack API base URL
     */
    private const API_BASE = 'https://patchstack.com/database/api/v2/';

    /**
     * API key for authentication
     */
    private string $api_key;

    /**
     * Cache prefix for API responses
     */
    private const CACHE_PREFIX = 'ultimate_security_patchstack_';

    /**
     * Cache duration (1 hour)
     */
    private const CACHE_DURATION = HOUR_IN_SECONDS;

    /**
     * Constructor
     *
     * @param string $api_key Patchstack API key
     */
    public function __construct(string $api_key)
    {
        $this->api_key = $api_key;
    }

    /**
     * Check a plugin for vulnerabilities
     *
     * @param string $slug Plugin slug
     * @param string $version Current plugin version
     * @return array|WP_Error Array of vulnerabilities or WP_Error on failure
     */
    public function check_plugin(string $slug, string $version)
    {
        $cache_key = self::CACHE_PREFIX . 'plugin_' . md5($slug . $version);
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        $response = $this->make_request('vulnerabilities', [
            'product_type' => 'plugin',
            'product_slug' => $slug,
        ]);

        if (is_wp_error($response)) {
            return $response;
        }

        $vulnerabilities = $this->parse_vulnerabilities($response, $version);

        set_transient($cache_key, $vulnerabilities, self::CACHE_DURATION);

        return $vulnerabilities;
    }

    /**
     * Check a theme for vulnerabilities
     *
     * @param string $slug Theme slug
     * @param string $version Current theme version
     * @return array|WP_Error Array of vulnerabilities or WP_Error on failure
     */
    public function check_theme(string $slug, string $version)
    {
        $cache_key = self::CACHE_PREFIX . 'theme_' . md5($slug . $version);
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        $response = $this->make_request('vulnerabilities', [
            'product_type' => 'theme',
            'product_slug' => $slug,
        ]);

        if (is_wp_error($response)) {
            return $response;
        }

        $vulnerabilities = $this->parse_vulnerabilities($response, $version);

        set_transient($cache_key, $vulnerabilities, self::CACHE_DURATION);

        return $vulnerabilities;
    }

    /**
     * Check WordPress core for vulnerabilities
     *
     * @param string $version WordPress version
     * @return array|WP_Error Array of vulnerabilities or WP_Error on failure
     */
    public function check_wordpress(string $version)
    {
        $cache_key = self::CACHE_PREFIX . 'wp_' . md5($version);
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        $response = $this->make_request('vulnerabilities', [
            'product_type' => 'wordpress',
            'affected_in' => $version,
        ]);

        if (is_wp_error($response)) {
            return $response;
        }

        $vulnerabilities = $this->parse_vulnerabilities($response, $version, true);

        set_transient($cache_key, $vulnerabilities, self::CACHE_DURATION);

        return $vulnerabilities;
    }

    /**
     * Make an API request
     *
     * @param string $endpoint API endpoint
     * @param array $params Query parameters
     * @return array|WP_Error Response data or WP_Error
     */
    private function make_request(string $endpoint, array $params = [])
    {
        $url = self::API_BASE . $endpoint;

        if (!empty($params)) {
            $url .= '?' . http_build_query($params);
        }

        $response = wp_remote_get($url, [
            'timeout' => 15,
            'headers' => [
                'Authorization' => 'Bearer ' . $this->api_key,
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
        ]);

        if (is_wp_error($response)) {
            return new WP_Error(
                'patchstack_request_failed',
                __('Failed to connect to Patchstack API.', 'ultimate-security')
            );
        }

        $code = wp_remote_retrieve_response_code($response);

        if ($code === 401 || $code === 403) {
            return new WP_Error(
                'patchstack_unauthorized',
                __('Invalid or expired Patchstack API key.', 'ultimate-security')
            );
        }

        if ($code === 429) {
            return new WP_Error(
                'patchstack_rate_limited',
                __('Patchstack API rate limit exceeded.', 'ultimate-security')
            );
        }

        if ($code === 404) {
            // No vulnerabilities found for this product
            return [];
        }

        if ($code !== 200) {
            return new WP_Error(
                'patchstack_api_error',
                sprintf(
                    // translators: %d is HTTP status code
                    __('Patchstack API returned error code: %d', 'ultimate-security'),
                    $code
                )
            );
        }

        $body = wp_remote_retrieve_body($response);
        $data = json_decode($body, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return new WP_Error(
                'patchstack_parse_error',
                __('Failed to parse Patchstack API response.', 'ultimate-security')
            );
        }

        return $data;
    }

    /**
     * Parse vulnerabilities from API response
     *
     * @param array $response API response
     * @param string $current_version Current installed version
     * @param bool $is_wordpress Whether this is WordPress core check
     * @return array Parsed vulnerabilities affecting the current version
     */
    private function parse_vulnerabilities(array $response, string $current_version, bool $is_wordpress = false): array
    {
        $vulnerabilities = [];

        if (empty($response) || empty($response['data'])) {
            return $vulnerabilities;
        }

        foreach ($response['data'] as $vuln) {
            // Check if this vulnerability affects the current version
            if (!$is_wordpress && !$this->version_affected($current_version, $vuln)) {
                continue;
            }

            $vulnerabilities[] = $this->format_vulnerability($vuln);
        }

        return $vulnerabilities;
    }

    /**
     * Check if a vulnerability affects the current version
     *
     * @param string $current_version Current installed version
     * @param array $vuln Vulnerability data
     * @return bool True if affected
     */
    private function version_affected(string $current_version, array $vuln): bool
    {
        // Check affected_in range if available
        if (!empty($vuln['affected_in'])) {
            $affected = $vuln['affected_in'];

            // Handle version range (e.g., "<= 1.2.3", "1.0.0 - 1.2.3")
            if (strpos($affected, '<=') !== false) {
                $max_version = trim(str_replace('<=', '', $affected));
                return version_compare($current_version, $max_version, '<=');
            }

            if (strpos($affected, '<') !== false) {
                $max_version = trim(str_replace('<', '', $affected));
                return version_compare($current_version, $max_version, '<');
            }

            if (strpos($affected, ' - ') !== false) {
                list($min, $max) = explode(' - ', $affected);
                return version_compare($current_version, trim($min), '>=') &&
                    version_compare($current_version, trim($max), '<=');
            }
        }

        // Check fixed_in version
        if (!empty($vuln['fixed_in'])) {
            return version_compare($current_version, $vuln['fixed_in'], '<');
        }

        // If no version info, assume affected
        return true;
    }

    /**
     * Format vulnerability data for consistent output
     *
     * @param array $vuln Raw vulnerability data from API
     * @return array Formatted vulnerability
     */
    private function format_vulnerability(array $vuln): array
    {
        $severity = $this->determine_severity($vuln);

        return [
            'id' => $vuln['id'] ?? null,
            'title' => $vuln['title'] ?? $vuln['name'] ?? __('Unknown vulnerability', 'ultimate-security'),
            'severity' => $severity,
            'cvss' => isset($vuln['cvss_score']) ? ['score' => $vuln['cvss_score']] : null,
            'cve' => $vuln['cve_id'] ?? $this->extract_cve($vuln),
            'fixed_in' => $vuln['fixed_in'] ?? $vuln['patched_in'] ?? null,
            'description' => $vuln['description'] ?? '',
            'vuln_type' => $vuln['vulnerability_type'] ?? $vuln['type'] ?? null,
            'published_date' => $vuln['disclosure_date'] ?? $vuln['published_at'] ?? null,
            'references' => $this->format_references($vuln),
            'source' => 'patchstack',
        ];
    }

    /**
     * Determine severity level from vulnerability data
     *
     * @param array $vuln Vulnerability data
     * @return string Severity level (critical, high, medium, low)
     */
    private function determine_severity(array $vuln): string
    {
        // Use severity field if available
        if (!empty($vuln['severity'])) {
            $severity = strtolower($vuln['severity']);
            if (in_array($severity, ['critical', 'high', 'medium', 'low'], true)) {
                return $severity;
            }
        }

        // Use CVSS score if available
        if (!empty($vuln['cvss_score'])) {
            $score = (float) $vuln['cvss_score'];

            if ($score >= 9.0) {
                return 'critical';
            }
            if ($score >= 7.0) {
                return 'high';
            }
            if ($score >= 4.0) {
                return 'medium';
            }
            return 'low';
        }

        // Fallback based on vulnerability type
        $vuln_type = strtolower($vuln['vulnerability_type'] ?? $vuln['type'] ?? '');

        if (preg_match('/rce|remote code|sql injection|file upload|deserialization/i', $vuln_type)) {
            return 'critical';
        }

        if (preg_match('/xss|csrf|auth bypass|privilege escalation|ssrf/i', $vuln_type)) {
            return 'high';
        }

        if (preg_match('/information disclosure|path traversal|redirect/i', $vuln_type)) {
            return 'medium';
        }

        return 'medium'; // Default
    }

    /**
     * Extract CVE ID from vulnerability data
     *
     * @param array $vuln Vulnerability data
     * @return string|null CVE ID or null
     */
    private function extract_cve(array $vuln): ?string
    {
        // Check direct CVE field
        if (!empty($vuln['cve'])) {
            return strpos($vuln['cve'], 'CVE-') === 0 ? $vuln['cve'] : 'CVE-' . $vuln['cve'];
        }

        // Check title or description for CVE pattern
        $search_text = ($vuln['title'] ?? '') . ' ' . ($vuln['description'] ?? '');
        if (preg_match('/CVE-\d{4}-\d+/i', $search_text, $matches)) {
            return strtoupper($matches[0]);
        }

        return null;
    }

    /**
     * Format references from vulnerability data
     *
     * @param array $vuln Vulnerability data
     * @return array Formatted references
     */
    private function format_references(array $vuln): array
    {
        $references = [];

        if (!empty($vuln['url'])) {
            $references['url'] = (array) $vuln['url'];
        }

        if (!empty($vuln['references'])) {
            if (is_string($vuln['references'])) {
                $references['url'] = array_merge(
                    $references['url'] ?? [],
                    array_filter(array_map('trim', explode("\n", $vuln['references'])))
                );
            } elseif (is_array($vuln['references'])) {
                $references = array_merge($references, $vuln['references']);
            }
        }

        if (!empty($vuln['cve_id'])) {
            $references['cve'] = [$vuln['cve_id']];
        }

        // Add Patchstack database link
        if (!empty($vuln['id'])) {
            $references['patchstack'] = ["https://patchstack.com/database/vulnerability/{$vuln['id']}"];
        }

        return $references;
    }

    /**
     * Validate API key by making a test request
     *
     * @return bool|WP_Error True if valid, WP_Error if not
     */
    public function validate_api_key()
    {
        $response = $this->make_request('vulnerabilities', [
            'product_type' => 'wordpress',
            'per_page' => 1,
        ]);

        if (is_wp_error($response)) {
            return $response;
        }

        return true;
    }

    /**
     * Search vulnerabilities by keyword
     *
     * @param string $keyword Search keyword
     * @param int $limit Maximum results
     * @return array|WP_Error Search results or WP_Error
     */
    public function search(string $keyword, int $limit = 10)
    {
        $cache_key = self::CACHE_PREFIX . 'search_' . md5($keyword . $limit);
        $cached = get_transient($cache_key);

        if ($cached !== false) {
            return $cached;
        }

        $response = $this->make_request('vulnerabilities/search', [
            'q' => $keyword,
            'per_page' => $limit,
        ]);

        if (is_wp_error($response)) {
            return $response;
        }

        $results = [];
        if (!empty($response['data'])) {
            foreach ($response['data'] as $vuln) {
                $results[] = $this->format_vulnerability($vuln);
            }
        }

        set_transient($cache_key, $results, self::CACHE_DURATION);

        return $results;
    }
}
