<?php

namespace WPUltimateSecurity\UltimateSecurity\Modules\Monitoring\VulnerabilityScanner;

use WP_Error;

/**
 * WPScan API Provider
 *
 * Integrates with the WPScan Vulnerability Database API
 * to check plugins, themes, and WordPress core for known vulnerabilities.
 *
 * @see https://wpscan.com/api
 */
class WPScanProvider implements VulnerabilityProviderInterface
{
    /**
     * WPScan API base URL
     */
    private const API_BASE = 'https://wpscan.com/api/v3/';

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

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

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

    /**
     * Constructor
     *
     * @param string $api_key WPScan 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("plugins/{$slug}");

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

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

        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("themes/{$slug}");

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

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

        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;
        }

        // WPScan API uses version format without dots for WordPress
        $version_key = str_replace('.', '', $version);
        $response = $this->make_request("wordpresses/{$version_key}");

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

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

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

        return $vulnerabilities;
    }

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

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

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

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

        if ($code === 401) {
            return new WP_Error(
                'wpscan_unauthorized',
                __('Invalid WPScan API key.', 'ultimate-security')
            );
        }

        if ($code === 403 || $code === 429) {
            return new WP_Error(
                'wpscan_rate_limited',
                __('WPScan API rate limit exceeded. Free tier allows 25 requests per day.', 'ultimate-security')
            );
        }

        if ($code === 404) {
            // Item not found in database - not an error
            return [];
        }

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

        // $body already retrieved above for logging
        $data = json_decode($body, true);

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

        return $data;
    }

    /**
     * Parse vulnerabilities from API response for plugins/themes
     *
     * @param array $response API response
     * @param string $current_version Current installed version
     * @param string $type 'plugin' or 'theme'
     * @return array Parsed vulnerabilities affecting the current version
     */
    private function parse_vulnerabilities(array $response, string $current_version, string $type): array
    {
        $vulnerabilities = [];

        if (empty($response)) {
            return $vulnerabilities;
        }

        // WPScan response structure: { "slug": { "vulnerabilities": [...] } }
        $item_data = reset($response);

        if (empty($item_data['vulnerabilities'])) {
            return $vulnerabilities;
        }

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

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

        return $vulnerabilities;
    }

    /**
     * Parse WordPress core vulnerabilities
     *
     * @param array $response API response
     * @param string $current_version Current WordPress version
     * @return array Parsed vulnerabilities
     */
    private function parse_wordpress_vulnerabilities(array $response, string $current_version): array
    {
        $vulnerabilities = [];

        if (empty($response)) {
            return $vulnerabilities;
        }

        // WPScan WordPress response structure
        $wp_data = reset($response);

        if (empty($wp_data['vulnerabilities'])) {
            return $vulnerabilities;
        }

        foreach ($wp_data['vulnerabilities'] as $vuln) {
            $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 (WPScan format)
        if (!empty($vuln['affected_in'])) {
            $affected = $vuln['affected_in'];

            // Handle version range (e.g., "<= 1.2.3", "< 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), '<=');
            }

            // Direct version match (affected version is exact or less)
            if (version_compare($current_version, trim($affected), '<=')) {
                return true;
            }
        }

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

        // If no version info, assume affected (conservative approach for security)
        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
    {
        // Determine severity from CVSS or vuln_type
        $severity = $this->determine_severity($vuln);

        return [
            'id' => $vuln['id'] ?? null,
            'title' => $vuln['title'] ?? __('Unknown vulnerability', 'ultimate-security'),
            'severity' => $severity,
            'cvss' => $vuln['cvss'] ?? null,
            'cve' => $this->extract_cve($vuln),
            'fixed_in' => $vuln['fixed_in'] ?? null,
            'description' => $vuln['description'] ?? '',
            'vuln_type' => $vuln['vuln_type'] ?? null,
            'published_date' => $vuln['published']['date'] ?? null,
            'references' => $vuln['references'] ?? [],
            'source' => 'wpscan',
        ];
    }

    /**
     * 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 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 to vulnerability type
        $vuln_type = strtolower($vuln['vuln_type'] ?? '');

        $critical_types = ['rce', 'remote code execution', 'sql injection', 'sqli', 'file upload'];
        $high_types = ['xss', 'cross-site scripting', 'csrf', 'authentication bypass', 'privilege escalation'];
        $medium_types = ['information disclosure', 'path traversal', 'open redirect'];

        foreach ($critical_types as $type) {
            if (strpos($vuln_type, $type) !== false) {
                return 'critical';
            }
        }

        foreach ($high_types as $type) {
            if (strpos($vuln_type, $type) !== false) {
                return 'high';
            }
        }

        foreach ($medium_types as $type) {
            if (strpos($vuln_type, $type) !== false) {
                return 'medium';
            }
        }

        return 'medium'; // Default to medium if uncertain
    }

    /**
     * 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 references for CVE
        if (!empty($vuln['references']['cve'])) {
            $cves = (array) $vuln['references']['cve'];
            return 'CVE-' . reset($cves);
        }

        // Check title for CVE pattern
        if (!empty($vuln['title']) && preg_match('/CVE-\d{4}-\d+/i', $vuln['title'], $matches)) {
            return strtoupper($matches[0]);
        }

        return null;
    }

    /**
     * Validate API key by making a test request
     *
     * @return bool|WP_Error True if valid, WP_Error if not
     */
    public function validate_api_key()
    {
        // Make a simple request to check API key validity
        $response = $this->make_request('wordpresses/600');

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

        return true;
    }

    /**
     * Get remaining API requests for the day (if available in response headers)
     *
     * @return int|null Remaining requests or null if unknown
     */
    public function get_remaining_requests(): ?int
    {
        $url = self::API_BASE . 'wordpresses/600';

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

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

        // WPScan returns rate limit info in headers
        $headers = wp_remote_retrieve_headers($response);

        if (isset($headers['x-ratelimit-remaining'])) {
            return (int) $headers['x-ratelimit-remaining'];
        }

        return null;
    }
}
