<?php

declare(strict_types=1);

namespace ShieldX\Scanner;

defined('ABSPATH') || exit;

use ShieldX\Storage\FileStorage;
use ShieldX\Support\WordPressPaths;

final class VulnerabilityScanner
{
    public const ENGINE_VERSION = 'shieldx-vulnerability-0.3.0';

    private const SETTINGS_OPTION = 'shieldx_vulnerability_scanner_settings';
    private const STATE_FILE = 'vulnerability-scan.json';
    private const ADVISORY_FILE = 'vulnerability-advisories.json';
    private const STALE_PLUGIN_DAYS = 730;
    private const ABANDONED_PLUGIN_DAYS = 1095;
    private const WPVULNERABILITY_ENDPOINT = 'https://www.wpvulnerability.net/';
    private const MAX_FEED_RESPONSE_BYTES = 5242880;
    private const MAX_WPVULNERABILITY_REQUESTS = 20;
    private const SOFTWARE_COMPONENTS = ['php', 'apache', 'nginx', 'mariadb', 'mysql', 'imagemagick', 'curl', 'memcached', 'redis', 'sqlite'];
    private const FEED_DNS_CACHE_SECONDS = 60;
    private const PLUGIN_REPOSITORY_METADATA_CACHE_SECONDS = 21600;

    /** @var ScanRepository */
    private $repository;

    /** @var FileStorage */
    private $storage;

    /** @var callable|null */
    private $inventoryProvider;

    /** @var array<int,string> */
    private $scanWarnings = [];

    /**
     * @param callable|null $inventoryProvider
     */
    public function __construct(ScanRepository $repository, FileStorage $storage, $inventoryProvider = null)
    {
        $this->repository = $repository;
        $this->storage = $storage;
        $this->inventoryProvider = is_callable($inventoryProvider) ? $inventoryProvider : null;
    }

    public static function fromWordPress(): self
    {
        return new self(ScanRepository::fromWordPress(), FileStorage::fromWordPress());
    }

    /**
     * @return array<string,mixed>
     */
    public function scan(bool $refreshFeed = false): array
    {
        $started = microtime(true);
        $this->scanWarnings = [];
        try {
            $this->storage->ensure();

            $inventory = $this->inventory();
            $settings = self::settings();
            $feedRefresh = $this->shouldRefreshAdvisoryFeed($settings, $refreshFeed)
                ? $this->refreshAdvisoryFeed($settings, $inventory)
                : $this->feedStatus();
            $advisories = $this->loadAdvisories();
            $issues = $this->evaluateInventory($inventory, $advisories);
            $activeThreatIds = [];

            foreach ($issues as $issue) {
                $activeThreatIds[] = (string) ($issue['threat_id'] ?? '');
                $this->repository->upsertIssue($issue);
            }

            $this->repository->resolveScannerIssuesForInspectedPaths($activeThreatIds, $this->inspectedPaths($inventory));

            $result = [
                'engine_version' => self::ENGINE_VERSION,
                'status' => 'completed',
                'last_scan_at' => $this->nowMysql(),
                'duration_ms' => (int) round((microtime(true) - $started) * 1000),
                'issues_found' => count($issues),
                'core_version' => (string) ($inventory['core']['version'] ?? ''),
                'plugins_total' => count((array) ($inventory['plugins'] ?? [])),
                'themes_total' => count((array) ($inventory['themes'] ?? [])),
                'software_total' => count((array) ($inventory['software'] ?? [])),
                'software_detected' => $this->countDetectedSoftware((array) ($inventory['software'] ?? [])),
                'updates_found' => $this->countUpdateIssues($issues),
                'stale_plugins' => $this->countIssuesByRule($issues, 'SX-VULN-PLUGIN-STALE'),
                'abandoned_plugins' => $this->countIssuesByRule($issues, 'SX-VULN-COMPONENT-ABANDONED'),
                'risky_version_ranges' => $this->countIssuesByRule($issues, 'SX-VULN-RISKY-VERSION'),
                'advisory_hits' => $this->countIssuesByRule($issues, 'SX-VULN-ADVISORY'),
                'software_advisory_hits' => $this->countIssuesByRuleAndPathPrefix($issues, 'SX-VULN-ADVISORY', 'software:'),
                'virtual_patch_recommendations' => $this->countIssuesByRule($issues, 'SX-VULN-VIRTUAL-PATCH'),
                'software' => $this->publicSoftwareInventory((array) ($inventory['software'] ?? [])),
                'advisory_cache' => [
                    'available' => !empty($advisories),
                    'updated_at' => (string) ($advisories['updated_at'] ?? ''),
                    'source' => (string) ($advisories['source'] ?? 'local-cache'),
                    'records_total' => (int) ($advisories['records_total'] ?? 0),
                    'last_refresh_status' => (string) ($feedRefresh['status'] ?? ''),
                    'last_error' => (string) ($feedRefresh['error'] ?? ''),
                    'last_warning' => $this->combinedWarning((string) ($feedRefresh['warning'] ?? '')),
                ],
                'feed_settings' => $this->publicFeedSettings($settings),
            ];

            $this->writeJson($this->statePath(), $result);

            return $result;
        } catch (\Throwable $error) {
            $this->writeFailedState($started, $error);

            throw $error;
        }
    }

    private function writeFailedState(float $started, \Throwable $error): void
    {
        $settings = self::settings();
        $advisories = [];
        try {
            $advisories = $this->loadAdvisories();
        } catch (\Throwable $advisoryError) {
            $this->recordScanWarning('Advisory cache status read failed after scanner error: ' . $advisoryError->getMessage());
        }

        $this->recordScanWarning('Vulnerability scan failed: ' . $error->getMessage());
        $failed = [
            'engine_version' => self::ENGINE_VERSION,
            'status' => 'failed',
            'last_scan_at' => $this->nowMysql(),
            'duration_ms' => (int) round((microtime(true) - $started) * 1000),
            'last_error' => $error->getMessage(),
            'advisory_cache' => [
                'available' => !empty($advisories),
                'updated_at' => (string) ($advisories['updated_at'] ?? ''),
                'source' => (string) ($advisories['source'] ?? 'local-cache'),
                'records_total' => (int) ($advisories['records_total'] ?? 0),
                'last_refresh_status' => 'failed',
                'last_error' => $error->getMessage(),
                'last_warning' => $this->combinedWarning(''),
            ],
            'feed_settings' => $this->publicFeedSettings($settings),
        ];

        try {
            $this->writeJson($this->statePath(), $failed);
        } catch (\Throwable $writeError) {
            unset($writeError);
        }
    }

    /**
     * @return array<string,mixed>
     */
    public function status(): array
    {
        $state = $this->readJson($this->statePath());
        $defaults = [
            'engine_version' => self::ENGINE_VERSION,
            'status' => 'not_run',
            'last_scan_at' => '',
            'duration_ms' => 0,
            'issues_found' => 0,
            'core_version' => '',
            'plugins_total' => 0,
            'themes_total' => 0,
            'software_total' => 0,
            'software_detected' => 0,
            'updates_found' => 0,
            'stale_plugins' => 0,
            'abandoned_plugins' => 0,
            'risky_version_ranges' => 0,
            'advisory_hits' => 0,
            'software_advisory_hits' => 0,
            'virtual_patch_recommendations' => 0,
            'software' => [],
            'advisory_cache' => [
                'available' => file_exists($this->advisoryPath()),
                'updated_at' => '',
                'source' => 'local-cache',
                'records_total' => 0,
                'last_refresh_status' => '',
                'last_error' => '',
                'last_warning' => '',
            ],
            'feed_settings' => $this->publicFeedSettings(self::settings()),
        ];

        $status = array_merge($defaults, $state);
        $advisoryCache = isset($status['advisory_cache']) && is_array($status['advisory_cache'])
            ? array_merge($defaults['advisory_cache'], $status['advisory_cache'])
            : $defaults['advisory_cache'];
        $advisories = $this->loadAdvisories();
        $advisoryCache['available'] = !empty($advisories);
        $status['advisory_cache'] = $advisoryCache;
        $status['feed_settings'] = $this->publicFeedSettings(self::settings());

        return $status;
    }

    /**
     * @return array<string,mixed>
     */
    public static function settings(): array
    {
        $defaults = [
            'feed_source' => 'wpvulnerability',
            'custom_feed_url' => '',
        ];

        if (!function_exists('get_option')) {
            return $defaults;
        }

        $settings = get_option(self::SETTINGS_OPTION, []);
        $settings = is_array($settings) ? $settings : [];

        return [
            'feed_source' => (string) ($settings['feed_source'] ?? $defaults['feed_source']),
            'custom_feed_url' => (string) ($settings['custom_feed_url'] ?? $defaults['custom_feed_url']),
        ];
    }

    /**
     * @param array<string,mixed> $settings
     */
    public static function saveSettings(array $settings): void
    {
        if (!function_exists('update_option')) {
            return;
        }

        $current = self::settings();
        $feedSource = (string) ($settings['feed_source'] ?? $current['feed_source']);
        if (!in_array($feedSource, ['wpvulnerability', 'local_cache', 'custom_json'], true)) {
            $feedSource = 'wpvulnerability';
        }

        $next = [
            'feed_source' => $feedSource,
            'custom_feed_url' => self::sanitizeUrl((string) ($settings['custom_feed_url'] ?? $current['custom_feed_url'])),
        ];

        update_option(self::SETTINGS_OPTION, $next, false);
    }

    private static function sanitizeUrl(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        if (function_exists('esc_url_raw')) {
            $url = (string) esc_url_raw($url);
        } else {
            $url = filter_var($url, FILTER_VALIDATE_URL) ? $url : '';
        }

        return self::isSafeFeedUrl($url) ? $url : '';
    }

    public static function isSafeFeedUrl(string $url): bool
    {
        $url = trim($url);
        if ($url === '' || !filter_var($url, FILTER_VALIDATE_URL)) {
            return false;
        }

        $parts = self::urlParts($url);
        if (!is_array($parts)) {
            return false;
        }

        $scheme = strtolower((string) ($parts['scheme'] ?? ''));
        $host = strtolower(trim((string) ($parts['host'] ?? ''), "[] \t\n\r\0\x0B"));

        if ($scheme !== 'https' || $host === '') {
            return false;
        }

        if ($host === 'localhost') {
            return false;
        }

        foreach (['.local', '.localhost', '.internal', '.home.arpa'] as $privateSuffix) {
            if (substr($host, -strlen($privateSuffix)) === $privateSuffix) {
                return false;
            }
        }

        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return self::isPublicIp($host);
        }

        if (strpos($host, '.') === false) {
            return false;
        }

        $addresses = self::publicFeedHostAddresses($host);
        if (is_array($addresses) && !empty($addresses)) {
            foreach ($addresses as $address) {
                if (!self::isPublicIp((string) $address)) {
                    return false;
                }
            }
        }

        return true;
    }

    private static function isPublicIp(string $ip): bool
    {
        return filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        ) !== false;
    }

    /**
     * @return array<string,string>|false
     */
    private static function urlParts(string $url)
    {
        if (function_exists('wp_parse_url')) {
            $parts = wp_parse_url($url);

            return is_array($parts) ? $parts : false;
        }

        if (preg_match('~^([a-z][a-z0-9+.-]*):\/\/(?:[^@\/?#]+@)?(\[[^\]]+\]|[^:\/?#]+)~i', $url, $matches) !== 1) {
            return false;
        }

        return [
            'scheme' => strtolower((string) $matches[1]),
            'host' => trim((string) $matches[2], '[]'),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function inventory(): array
    {
        if ($this->inventoryProvider) {
            $inventory = call_user_func($this->inventoryProvider);

            return is_array($inventory) ? $this->normalizeInventory($inventory) : $this->emptyInventory();
        }

        return $this->wordpressInventory();
    }

    /**
     * @return array<string,mixed>
     */
    private function wordpressInventory(): array
    {
        $this->loadWordPressAdminIncludes();

        return $this->normalizeInventory([
            'core' => [
                'slug' => 'wordpress',
                'name' => 'WordPress',
                'version' => $this->wordpressVersion(),
                'update' => $this->coreUpdate(),
            ],
            'plugins' => $this->pluginInventory(),
            'themes' => $this->themeInventory(),
            'software' => $this->softwareInventory(),
        ]);
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function pluginInventory(): array
    {
        if (!function_exists('get_plugins')) {
            return [];
        }

        $updates = $this->pluginUpdates();
        $plugins = [];
        try {
            $installedPlugins = (array) get_plugins();
        } catch (\Throwable $error) {
            $this->recordScanWarning('Plugin inventory read failed: ' . $error->getMessage());

            return [];
        }

        foreach ($installedPlugins as $file => $data) {
            $file = (string) $file;
            $slug = self::slugFromPluginFile($file);
            $pluginData = is_array($data) ? $data : [];
            $metadata = [];
            $active = false;
            try {
                $metadata = $this->pluginRepositoryMetadata($slug);
                $active = function_exists('is_plugin_active') ? (bool) is_plugin_active($file) : false;
            } catch (\Throwable $error) {
                $this->recordScanWarning(sprintf('Plugin inventory item failed for %s: %s', $slug !== '' ? $slug : $file, $error->getMessage()));
            }

            $plugins[] = [
                'slug' => $slug,
                'file' => $file,
                'name' => (string) ($pluginData['Name'] ?? $slug),
                'version' => (string) ($pluginData['Version'] ?? ''),
                'active' => $active,
                'source' => !empty($metadata) ? 'wordpress.org' : 'local',
                'metadata' => $metadata,
                'update' => $updates[$file] ?? $updates[$slug] ?? [],
            ];
        }

        return $plugins;
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function themeInventory(): array
    {
        if (!function_exists('wp_get_themes')) {
            return [];
        }

        $updates = $this->themeUpdates();
        $themes = [];
        try {
            $installedThemes = (array) wp_get_themes();
        } catch (\Throwable $error) {
            $this->recordScanWarning('Theme inventory read failed: ' . $error->getMessage());

            return [];
        }

        foreach ($installedThemes as $slug => $theme) {
            $slug = (string) $slug;
            $metadata = [];
            $name = $slug;
            $version = '';
            try {
                $metadata = $this->themeRepositoryMetadata($slug);
                $name = is_object($theme) && method_exists($theme, 'get') ? (string) $theme->get('Name') : $slug;
                $version = is_object($theme) && method_exists($theme, 'get') ? (string) $theme->get('Version') : '';
            } catch (\Throwable $error) {
                $this->recordScanWarning(sprintf('Theme inventory item failed for %s: %s', $slug, $error->getMessage()));
            }

            $themes[] = [
                'slug' => $slug,
                'name' => $name,
                'version' => $version,
                'source' => !empty($metadata) ? 'wordpress.org' : 'local',
                'metadata' => $metadata,
                'update' => $updates[$slug] ?? [],
            ];
        }

        return $themes;
    }

    /**
     * @return array<string,mixed>
     */
    private function coreUpdate(): array
    {
        if (function_exists('wp_version_check')) {
            try {
                wp_version_check([], true);
            } catch (\Throwable $error) {
                $this->recordScanWarning('Core update metadata refresh failed: ' . $error->getMessage());
            }
        }

        if (!function_exists('get_core_updates')) {
            return [];
        }

        try {
            $coreUpdates = get_core_updates(['dismissed' => false]);
        } catch (\Throwable $error) {
            $this->recordScanWarning('Core update metadata read failed: ' . $error->getMessage());

            return [];
        }

        foreach ((array) $coreUpdates as $update) {
            $response = is_object($update) ? (string) ($update->response ?? '') : '';
            $current = is_object($update) ? (string) ($update->current ?? '') : '';

            if ($response === 'upgrade' && $current !== '') {
                return [
                    'new_version' => $current,
                    'response' => $response,
                ];
            }
        }

        return [];
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    private function pluginUpdates(): array
    {
        if (function_exists('wp_update_plugins')) {
            try {
                wp_update_plugins();
            } catch (\Throwable $error) {
                $this->recordScanWarning('Plugin update metadata refresh failed: ' . $error->getMessage());
            }
        }

        if (!function_exists('get_plugin_updates')) {
            return [];
        }

        $updates = [];
        try {
            $pluginUpdates = get_plugin_updates();
        } catch (\Throwable $error) {
            $this->recordScanWarning('Plugin update metadata read failed: ' . $error->getMessage());

            return [];
        }

        foreach ((array) $pluginUpdates as $file => $data) {
            $update = is_object($data) && isset($data->update) && is_object($data->update) ? $data->update : null;
            $newVersion = $update ? (string) ($update->new_version ?? '') : '';
            $slug = $update ? (string) ($update->slug ?? '') : '';

            if ($newVersion === '') {
                continue;
            }

            $payload = [
                'new_version' => $newVersion,
                'slug' => $slug,
            ];
            $updates[(string) $file] = $payload;
            if ($slug !== '') {
                $updates[$slug] = $payload;
            }
        }

        return $updates;
    }

    /**
     * @return array<string,array<string,mixed>>
     */
    private function themeUpdates(): array
    {
        if (function_exists('wp_update_themes')) {
            try {
                wp_update_themes();
            } catch (\Throwable $error) {
                $this->recordScanWarning('Theme update metadata refresh failed: ' . $error->getMessage());
            }
        }

        if (!function_exists('get_theme_updates')) {
            return [];
        }

        $updates = [];
        try {
            $themeUpdates = get_theme_updates();
        } catch (\Throwable $error) {
            $this->recordScanWarning('Theme update metadata read failed: ' . $error->getMessage());

            return [];
        }

        foreach ((array) $themeUpdates as $slug => $data) {
            $update = is_object($data) && isset($data->update) ? $data->update : [];
            $newVersion = is_array($update) ? (string) ($update['new_version'] ?? '') : (is_object($update) ? (string) ($update->new_version ?? '') : '');

            if ($newVersion !== '') {
                $updates[(string) $slug] = ['new_version' => $newVersion];
            }
        }

        return $updates;
    }

    private function recordScanWarning(string $warning): void
    {
        $warning = trim($warning);
        if ($warning === '' || in_array($warning, $this->scanWarnings, true)) {
            return;
        }

        $this->scanWarnings[] = $warning;
    }

    private function combinedWarning(string $primaryWarning): string
    {
        $warnings = [];
        $primaryWarning = trim($primaryWarning);
        if ($primaryWarning !== '') {
            $warnings[] = $primaryWarning;
        }

        foreach ($this->scanWarnings as $warning) {
            if (!in_array($warning, $warnings, true)) {
                $warnings[] = $warning;
            }
        }

        return implode('; ', array_slice($warnings, 0, 3));
    }

    /**
     * @return array<string,mixed>
     */
    private function pluginRepositoryMetadata(string $slug): array
    {
        if ($slug === '' || in_array($slug, ['shield-x', 'xerotact-shield-x-security'], true) || !function_exists('plugins_api')) {
            return [];
        }

        $cacheKey = self::pluginRepositoryMetadataCacheKey($slug);
        if (function_exists('get_transient')) {
            try {
                $cached = get_transient($cacheKey);
                if (is_array($cached) && isset($cached['metadata']) && is_array($cached['metadata'])) {
                    return $cached['metadata'];
                }
            } catch (\Throwable $error) {
                $this->recordScanWarning(sprintf('Plugin repository metadata cache read failed for %s: %s', $slug, $error->getMessage()));
            }
        }

        try {
            $info = plugins_api('plugin_information', [
                'slug' => $slug,
                'fields' => [
                    'last_updated' => true,
                    'requires' => true,
                    'requires_php' => true,
                    'tested' => true,
                    'sections' => false,
                    'versions' => false,
                    'banners' => false,
                    'icons' => false,
                ],
            ]);
        } catch (\Throwable $error) {
            $this->recordScanWarning(sprintf('Plugin repository metadata lookup failed for %s: %s', $slug, $error->getMessage()));
            $this->cachePluginRepositoryMetadata($cacheKey, []);

            return [];
        }

        if (function_exists('is_wp_error') && is_wp_error($info)) {
            $this->cachePluginRepositoryMetadata($cacheKey, []);
            return [];
        }

        if (!is_object($info)) {
            $this->cachePluginRepositoryMetadata($cacheKey, []);
            return [];
        }

        $metadata = [
            'last_updated' => (string) ($info->last_updated ?? ''),
            'requires' => (string) ($info->requires ?? ''),
            'requires_php' => (string) ($info->requires_php ?? ''),
            'tested' => (string) ($info->tested ?? ''),
        ];
        $this->cachePluginRepositoryMetadata($cacheKey, $metadata);

        return $metadata;
    }

    private static function pluginRepositoryMetadataCacheKey(string $slug): string
    {
        return 'shieldx_vuln_plugin_meta_' . md5($slug);
    }

    /**
     * @param array<string,mixed> $metadata
     */
    private function cachePluginRepositoryMetadata(string $cacheKey, array $metadata): void
    {
        if (function_exists('set_transient')) {
            try {
                set_transient($cacheKey, ['metadata' => $metadata], self::PLUGIN_REPOSITORY_METADATA_CACHE_SECONDS);
            } catch (\Throwable $error) {
                $this->recordScanWarning('Plugin repository metadata cache write failed: ' . $error->getMessage());
            }
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function themeRepositoryMetadata(string $slug): array
    {
        if ($slug === '' || !function_exists('themes_api')) {
            return [];
        }

        try {
            $info = themes_api('theme_information', [
                'slug' => $slug,
                'fields' => [
                    'last_updated' => true,
                    'requires' => true,
                    'requires_php' => true,
                    'tested' => true,
                    'sections' => false,
                    'tags' => false,
                ],
            ]);
        } catch (\Throwable $error) {
            $this->recordScanWarning(sprintf('Theme repository metadata lookup failed for %s: %s', $slug, $error->getMessage()));

            return [];
        }

        if (function_exists('is_wp_error') && is_wp_error($info)) {
            return [];
        }

        if (!is_object($info)) {
            return [];
        }

        return [
            'last_updated' => (string) ($info->last_updated ?? ''),
            'requires' => (string) ($info->requires ?? ''),
            'requires_php' => (string) ($info->requires_php ?? ''),
            'tested' => (string) ($info->tested ?? ''),
        ];
    }

    private function wordpressVersion(): string
    {
        if (function_exists('get_bloginfo')) {
            return (string) get_bloginfo('version');
        }

        global $wp_version;

        return is_string($wp_version) ? $wp_version : '';
    }

    /**
     * @return array<int,array<string,mixed>>
     */
    private function softwareInventory(): array
    {
        $items = [];
        $webServer = $this->webServerComponent();
        if ($webServer) {
            $items[] = $webServer;
        }

        $database = $this->databaseComponent();
        if ($database) {
            $items[] = $database;
        }

        $items[] = $this->softwareComponent('php', 'PHP', $this->cleanVersion(PHP_VERSION), 'PHP_VERSION', 100);
        $items[] = $this->softwareComponent('curl', 'curl', $this->curlVersion(), 'curl_version()', 90);
        $items[] = $this->softwareComponent('imagemagick', 'ImageMagick', $this->imagickVersion(), 'Imagick extension', 90);
        $items[] = $this->softwareComponent('redis', 'Redis', $this->extensionVersion('redis'), 'PHP redis extension', 60);
        $items[] = $this->softwareComponent('memcached', 'Memcached', $this->memcachedVersion(), 'PHP memcached extension', 60);
        $items[] = $this->softwareComponent('sqlite', 'SQLite', $this->sqliteVersion(), 'PHP SQLite extension', 80);

        $deduped = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }

            $slug = (string) ($item['slug'] ?? '');
            if ($slug !== '') {
                $deduped[$slug] = $item;
            }
        }

        return array_values($deduped);
    }

    /**
     * @return array<string,mixed>|null
     */
    private function webServerComponent(): ?array
    {
        $server = $this->serverSoftware();
        if ($server === '') {
            return null;
        }

        if (preg_match('/apache(?:\/|\s+)([0-9][0-9A-Za-z.\-+]*)/i', $server, $matches) === 1) {
            return $this->softwareComponent('apache', 'Apache HTTPD', $this->cleanVersion((string) $matches[1]), 'SERVER_SOFTWARE', 80);
        }

        if (preg_match('/nginx(?:\/|\s+)([0-9][0-9A-Za-z.\-+]*)/i', $server, $matches) === 1) {
            return $this->softwareComponent('nginx', 'nginx', $this->cleanVersion((string) $matches[1]), 'SERVER_SOFTWARE', 80);
        }

        return null;
    }

    private function serverSoftware(): string
    {
        return $this->serverText('SERVER_SOFTWARE');
    }

    private function serverText(string $key): string
    {
        if (!isset($_SERVER[$key]) || !is_scalar($_SERVER[$key])) {
            return '';
        }

        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Dynamic server value is unslashed here and sanitized before return.
        $value = function_exists('wp_unslash') ? wp_unslash($_SERVER[$key]) : $_SERVER[$key];
        $value = preg_replace('/[\r\n\x00-\x1F\x7F]+/', '', (string) $value);
        $value = is_string($value) ? trim($value) : '';

        return function_exists('sanitize_text_field') ? sanitize_text_field($value) : $value;
    }

    /**
     * @return array<string,mixed>|null
     */
    private function databaseComponent(): ?array
    {
        $version = '';
        global $wpdb;
        if (is_object($wpdb)) {
            if (method_exists($wpdb, 'db_version')) {
                $version = (string) $wpdb->db_version();
            }
        }

        if ($version === '') {
            return null;
        }

        $slug = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql';
        $label = $slug === 'mariadb' ? 'MariaDB' : 'MySQL';

        return $this->softwareComponent($slug, $label, $this->cleanVersion($version), 'Database server version', 90);
    }

    /**
     * @return array<string,mixed>
     */
    private function softwareComponent(string $slug, string $name, string $version, string $method, int $reliability): array
    {
        return [
            'slug' => $slug,
            'name' => $name,
            'version' => $version,
            'method' => $method,
            'reliability' => max(0, min(100, $reliability)),
            'detected' => $version !== '',
        ];
    }

    private function curlVersion(): string
    {
        if (!function_exists('curl_version')) {
            return '';
        }

        $info = curl_version();

        return is_array($info) ? $this->cleanVersion((string) ($info['version'] ?? '')) : '';
    }

    private function imagickVersion(): string
    {
        if (!class_exists('Imagick')) {
            return '';
        }

        try {
            $version = \Imagick::getVersion();
        } catch (\Throwable $error) {
            return '';
        }

        if (!is_array($version)) {
            return '';
        }

        return $this->cleanVersion((string) ($version['versionString'] ?? ''));
    }

    private function memcachedVersion(): string
    {
        $version = $this->extensionVersion('memcached');
        if ($version !== '') {
            return $version;
        }

        return $this->extensionVersion('memcache');
    }

    private function sqliteVersion(): string
    {
        if (class_exists('SQLite3')) {
            $version = \SQLite3::version();
            if (is_array($version)) {
                return $this->cleanVersion((string) ($version['versionString'] ?? ''));
            }
        }

        return $this->extensionVersion('sqlite3');
    }

    private function extensionVersion(string $extension): string
    {
        if (!extension_loaded($extension)) {
            return '';
        }

        $version = phpversion($extension);

        return is_string($version) ? $this->cleanVersion($version) : '';
    }

    private function cleanVersion(string $value): string
    {
        if (preg_match('/([0-9]+(?:\.[0-9A-Za-z][0-9A-Za-z.\-+]*)?)/', $value, $matches) !== 1) {
            return '';
        }

        return trim((string) $matches[1], '.-+');
    }

    private function loadWordPressAdminIncludes(): void
    {
        foreach ([
            'plugin.php',
            'update.php',
            'plugin-install.php',
            'theme-install.php',
            'theme.php',
        ] as $relative) {
            $path = WordPressPaths::adminIncludesPath($relative);
            if (is_file($path)) {
                require_once $path;
            }
        }
    }

    /**
     * @param array<string,mixed> $inventory
     * @param array<string,mixed> $advisories
     * @return array<int,array<string,mixed>>
     */
    private function evaluateInventory(array $inventory, array $advisories): array
    {
        $issues = [];
        $core = isset($inventory['core']) && is_array($inventory['core']) ? $inventory['core'] : [];
        $coreVersion = (string) ($core['version'] ?? '');

        if ($this->hasUpdate($core)) {
            $issues[] = $this->issue(
                'SX-VULN-CORE-UPDATE',
                'vulnerability_core_update',
                'high',
                'WordPress core update is available.',
                'core:wordpress',
                sprintf('Installed %s, available %s.', (string) ($core['version'] ?? ''), (string) ($core['update']['new_version'] ?? ''))
            );
        }

        $issues = array_merge($issues, $this->riskyVersionIssues('core', $core, 'core:wordpress'));
        $issues = array_merge($issues, $this->advisoryIssuesForTarget('core', 'wordpress', 'core:wordpress', $coreVersion, $advisories));

        foreach ((array) ($inventory['plugins'] ?? []) as $plugin) {
            if (!is_array($plugin)) {
                continue;
            }

            $slug = (string) ($plugin['slug'] ?? '');
            $path = 'plugin:' . $slug;
            if ($slug === '') {
                continue;
            }

            if ($this->hasUpdate($plugin)) {
                $issues[] = $this->issue(
                    'SX-VULN-PLUGIN-UPDATE',
                    'vulnerability_plugin_update',
                    'medium',
                    sprintf('Plugin update is available for %s.', (string) ($plugin['name'] ?? $slug)),
                    $path,
                    sprintf('Installed %s, available %s.', (string) ($plugin['version'] ?? ''), (string) ($plugin['update']['new_version'] ?? ''))
                );
            }

            $issues = array_merge($issues, $this->repositoryHealthIssues('plugin', $plugin, $path, $coreVersion));
            $issues = array_merge($issues, $this->riskyVersionIssues('plugin', $plugin, $path));

            $issues = array_merge($issues, $this->advisoryIssuesForTarget('plugins', $slug, $path, (string) ($plugin['version'] ?? ''), $advisories));
        }

        foreach ((array) ($inventory['themes'] ?? []) as $theme) {
            if (!is_array($theme)) {
                continue;
            }

            $slug = (string) ($theme['slug'] ?? '');
            $path = 'theme:' . $slug;
            if ($slug === '') {
                continue;
            }

            if ($this->hasUpdate($theme)) {
                $issues[] = $this->issue(
                    'SX-VULN-THEME-UPDATE',
                    'vulnerability_theme_update',
                    'medium',
                    sprintf('Theme update is available for %s.', (string) ($theme['name'] ?? $slug)),
                    $path,
                    sprintf('Installed %s, available %s.', (string) ($theme['version'] ?? ''), (string) ($theme['update']['new_version'] ?? ''))
                );
            }

            $issues = array_merge($issues, $this->repositoryHealthIssues('theme', $theme, $path, $coreVersion));
            $issues = array_merge($issues, $this->riskyVersionIssues('theme', $theme, $path));
            $issues = array_merge($issues, $this->advisoryIssuesForTarget('themes', $slug, $path, (string) ($theme['version'] ?? ''), $advisories));
        }

        foreach ((array) ($inventory['software'] ?? []) as $component) {
            if (!is_array($component)) {
                continue;
            }

            $slug = (string) ($component['slug'] ?? '');
            $version = (string) ($component['version'] ?? '');
            if ($slug === '' || $version === '') {
                continue;
            }

            $issues = array_merge($issues, $this->advisoryIssuesForTarget('software', $slug, 'software:' . $slug, $version, $advisories));
        }

        return $issues;
    }

    /**
     * @param array<string,mixed> $advisories
     * @return array<int,array<string,mixed>>
     */
    private function advisoryIssuesForTarget(string $bucket, string $slug, string $path, string $version, array $advisories): array
    {
        $items = [];
        $targetAdvisories = [];

        if ($bucket === 'core' && isset($advisories['core']) && is_array($advisories['core'])) {
            $targetAdvisories = $advisories['core'];
        } elseif (isset($advisories[$bucket][$slug]) && is_array($advisories[$bucket][$slug])) {
            $targetAdvisories = $advisories[$bucket][$slug];
        }

        foreach ($targetAdvisories as $advisory) {
            if (!is_array($advisory) || !self::advisoryAffectsVersion($advisory, $version)) {
                continue;
            }

            $id = self::advisoryScalar($advisory, 'id', self::advisoryScalar($advisory, 'cve', sha1($path . $version)));
            $title = self::advisoryScalar($advisory, 'title', 'Known vulnerability advisory');
            $severity = $this->normalizeSeverity(self::advisoryScalar($advisory, 'severity', 'medium'));
            $fixedSource = $advisory['fixed_in'] ?? $advisory['patched_in'] ?? '';
            if (is_array($fixedSource)) {
                $fixedIn = implode(', ', array_values(array_filter(array_map(static function ($item): string {
                    return is_scalar($item) ? (string) $item : '';
                }, $fixedSource), static function (string $item): bool {
                    return $item !== '';
                })));
            } else {
                $fixedIn = is_scalar($fixedSource) ? (string) $fixedSource : '';
            }
            $badges = $this->advisoryBadges($advisory);
            $evidence = trim(sprintf('%s affects %s %s.%s', $id, $slug, $version, $fixedIn !== '' ? ' Fixed in ' . $fixedIn . '.' : ''));
            if (!empty($badges)) {
                $evidence .= ' Signals: ' . implode(', ', $badges) . '.';
            }
            $remediation = self::advisoryScalar($advisory, 'remediation');
            if ($remediation !== '') {
                $evidence .= ' Remediation: ' . $remediation;
            }

            $items[] = $this->issue(
                'SX-VULN-ADVISORY',
                'vulnerability_advisory',
                $severity,
                $title,
                $path,
                $evidence,
                $id
            );

            if ($this->needsVirtualPatch($advisory, $severity)) {
                $items[] = $this->issue(
                    'SX-VULN-VIRTUAL-PATCH',
                    'vulnerability_virtual_patch',
                    $severity,
                    sprintf('Virtual patch recommended for %s.', $slug),
                    $path,
                    $this->virtualPatchRecommendation($slug, $bucket, $advisory),
                    $id
                );
            }
        }

        return $items;
    }

    /**
     * @param array<string,mixed> $advisory
     */
    private static function advisoryScalar(array $advisory, string $key, string $default = ''): string
    {
        $value = $advisory[$key] ?? $default;

        return is_scalar($value) ? (string) $value : $default;
    }

    /**
     * @param array<string,mixed> $component
     * @return array<int,array<string,mixed>>
     */
    private function repositoryHealthIssues(string $kind, array $component, string $path, string $coreVersion): array
    {
        $issues = [];
        $name = (string) ($component['name'] ?? $component['slug'] ?? $kind);
        $source = (string) ($component['source'] ?? '');
        $metadata = isset($component['metadata']) && is_array($component['metadata']) ? $component['metadata'] : [];
        $lastUpdated = (string) ($metadata['last_updated'] ?? $component['last_updated'] ?? '');

        if ($lastUpdated !== '' && self::isStaleLastUpdated($lastUpdated, null, self::ABANDONED_PLUGIN_DAYS)) {
            $issues[] = $this->issue(
                'SX-VULN-COMPONENT-ABANDONED',
                'vulnerability_' . $kind . '_abandoned',
                'high',
                sprintf('%s appears abandoned: %s.', ucfirst($kind), $name),
                $path,
                sprintf('Last WordPress.org update was %s, more than %d days ago.', $lastUpdated, self::ABANDONED_PLUGIN_DAYS)
            );
        } elseif ($lastUpdated !== '' && self::isStaleLastUpdated($lastUpdated)) {
            $issues[] = $this->issue(
                'SX-VULN-PLUGIN-STALE',
                'vulnerability_' . $kind . '_stale',
                'medium',
                sprintf('%s appears stale: %s.', ucfirst($kind), $name),
                $path,
                sprintf('Last WordPress.org update was %s.', $lastUpdated)
            );
        }

        $tested = (string) ($metadata['tested'] ?? '');
        if ($tested !== '' && $coreVersion !== '' && version_compare($tested, $this->majorMinor($coreVersion), '<')) {
            $issues[] = $this->issue(
                'SX-VULN-COMPONENT-UNTESTED',
                'vulnerability_' . $kind . '_untested',
                'low',
                sprintf('%s has not been tested with this WordPress version: %s.', ucfirst($kind), $name),
                $path,
                sprintf('WordPress.org tested up to %s, installed core is %s.', $tested, $coreVersion)
            );
        }

        if ($source === 'local' && $kind !== 'core') {
            $issues[] = $this->issue(
                'SX-VULN-COMPONENT-UNLISTED',
                'vulnerability_' . $kind . '_unlisted',
                'low',
                sprintf('%s is not matched to WordPress.org metadata: %s.', ucfirst($kind), $name),
                $path,
                'SHIELD-X cannot verify repository age, tested version, or official update status for this local/commercial component.'
            );
        }

        return $issues;
    }

    /**
     * @param array<string,mixed> $component
     * @return array<int,array<string,mixed>>
     */
    private function riskyVersionIssues(string $kind, array $component, string $path): array
    {
        $current = (string) ($component['version'] ?? '');
        $new = (string) ($component['update']['new_version'] ?? '');
        if ($current === '' || $new === '' || !version_compare($new, $current, '>')) {
            return [];
        }

        $gap = $this->versionGap($current, $new);
        if ($gap === '') {
            return [];
        }

        $name = (string) ($component['name'] ?? $component['slug'] ?? $kind);

        return [$this->issue(
            'SX-VULN-RISKY-VERSION',
            'vulnerability_' . $kind . '_risky_version',
            $gap === 'major' ? 'high' : 'medium',
            sprintf('%s is behind by a %s version range: %s.', ucfirst($kind), $gap, $name),
            $path,
            sprintf('Installed %s, available %s. Larger version gaps usually mean more accumulated security fixes and higher update risk.', $current, $new)
        )];
    }

    /**
     * @param array<string,mixed> $advisory
     */
    private function needsVirtualPatch(array $advisory, string $severity): bool
    {
        $patched = array_key_exists('patched', $advisory) ? !empty($advisory['patched']) : true;
        $fixed = array_filter(array_map(static function ($item): string {
            return is_scalar($item) ? (string) $item : '';
        }, (array) ($advisory['fixed_in'] ?? $advisory['patched_in'] ?? [])));
        $exploitation = strtolower(self::advisoryScalar($advisory, 'exploitation'));
        $automatable = strtolower(self::advisoryScalar($advisory, 'automatable'));

        return !$patched
            || empty($fixed)
            || in_array($severity, ['critical', 'high'], true)
            || !empty($advisory['kev'])
            || in_array($exploitation, ['active', 'poc'], true)
            || $automatable === 'yes';
    }

    /**
     * @param array<string,mixed> $advisory
     */
    private function virtualPatchRecommendation(string $slug, string $bucket, array $advisory): string
    {
        $references = isset($advisory['references']) && is_array($advisory['references'])
            ? implode(', ', array_slice(array_map('strval', $advisory['references']), 0, 2))
            : '';
        $type = $bucket === 'plugins' ? 'plugin' : ($bucket === 'themes' ? 'theme' : ($bucket === 'software' ? 'software component' : 'core'));
        $remediation = (string) ($advisory['remediation'] ?? '');

        $message = sprintf(
            'Virtual patch recommendation: keep WAF in enforce mode for this site, monitor requests touching the vulnerable %s "%s", and disable or update the component if exploitation appears.',
            $type,
            $slug
        );

        if ($remediation !== '') {
            $message .= ' Vendor remediation: ' . $remediation;
        }

        if ($references !== '') {
            $message .= ' References: ' . $references;
        }

        return $message;
    }

    /**
     * @param array<string,mixed> $target
     */
    private function hasUpdate(array $target): bool
    {
        $current = (string) ($target['version'] ?? '');
        $new = (string) ($target['update']['new_version'] ?? '');

        return $current !== '' && $new !== '' && version_compare($new, $current, '>');
    }

    /**
     * @param array<string,mixed> $issue
     * @return array<string,mixed>
     */
    private function issue(string $ruleId, string $type, string $severity, string $description, string $path, string $evidence, string $variant = ''): array
    {
        return [
            'threat_id' => 'sx-' . substr(sha1($ruleId . '|' . $path . '|' . $variant), 0, 32),
            'rule_id' => $ruleId,
            'type' => $type,
            'severity' => $this->normalizeSeverity($severity),
            'description' => $description,
            'affected_path' => $path,
            'evidence' => $evidence,
            'source' => 'scanner',
        ];
    }

    /**
     * @param array<string,mixed> $inventory
     * @return array<int,string>
     */
    private function inspectedPaths(array $inventory): array
    {
        $paths = ['core:wordpress'];

        foreach ((array) ($inventory['plugins'] ?? []) as $plugin) {
            if (is_array($plugin) && (string) ($plugin['slug'] ?? '') !== '') {
                $paths[] = 'plugin:' . (string) $plugin['slug'];
            }
        }

        foreach ((array) ($inventory['themes'] ?? []) as $theme) {
            if (is_array($theme) && (string) ($theme['slug'] ?? '') !== '') {
                $paths[] = 'theme:' . (string) $theme['slug'];
            }
        }

        foreach ((array) ($inventory['software'] ?? []) as $component) {
            if (is_array($component) && (string) ($component['slug'] ?? '') !== '') {
                $paths[] = 'software:' . (string) $component['slug'];
            }
        }

        return array_values(array_unique($paths));
    }

    /**
     * @param array<string,mixed> $inventory
     * @return array<string,mixed>
     */
    private function normalizeInventory(array $inventory): array
    {
        $normalized = $this->emptyInventory();
        $core = isset($inventory['core']) && is_array($inventory['core']) ? $inventory['core'] : [];
        $normalized['core'] = array_merge($normalized['core'], $core);

        foreach ((array) ($inventory['plugins'] ?? []) as $plugin) {
            if (!is_array($plugin)) {
                continue;
            }

            $slug = (string) ($plugin['slug'] ?? '');
            if ($slug === '' && (string) ($plugin['file'] ?? '') !== '') {
                $slug = self::slugFromPluginFile((string) $plugin['file']);
            }

            if ($slug === '') {
                continue;
            }

            $plugin['slug'] = $slug;
            $plugin['metadata'] = isset($plugin['metadata']) && is_array($plugin['metadata']) ? $plugin['metadata'] : [];
            $plugin['update'] = isset($plugin['update']) && is_array($plugin['update']) ? $plugin['update'] : [];
            $normalized['plugins'][] = $plugin;
        }

        foreach ((array) ($inventory['themes'] ?? []) as $theme) {
            if (!is_array($theme) || (string) ($theme['slug'] ?? '') === '') {
                continue;
            }

            $theme['update'] = isset($theme['update']) && is_array($theme['update']) ? $theme['update'] : [];
            $theme['metadata'] = isset($theme['metadata']) && is_array($theme['metadata']) ? $theme['metadata'] : [];
            $normalized['themes'][] = $theme;
        }

        foreach ((array) ($inventory['software'] ?? []) as $component) {
            if (!is_array($component)) {
                continue;
            }

            $slug = (string) ($component['slug'] ?? '');
            if ($slug === '' || !in_array($slug, self::SOFTWARE_COMPONENTS, true)) {
                continue;
            }

            $component['slug'] = $slug;
            $component['name'] = (string) ($component['name'] ?? $slug);
            $component['version'] = $this->cleanVersion((string) ($component['version'] ?? ''));
            $component['method'] = (string) ($component['method'] ?? '');
            $component['detected'] = !empty($component['detected']) || $component['version'] !== '';
            $component['reliability'] = max(0, min(100, (int) ($component['reliability'] ?? 0)));
            $normalized['software'][] = $component;
        }

        return $normalized;
    }

    /**
     * @return array<string,mixed>
     */
    private function emptyInventory(): array
    {
        return [
            'core' => [
                'slug' => 'wordpress',
                'name' => 'WordPress',
                'version' => '',
                'update' => [],
            ],
            'plugins' => [],
            'themes' => [],
            'software' => [],
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function loadAdvisories(): array
    {
        return $this->readSignedAdvisories($this->advisoryPath());
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private function shouldRefreshAdvisoryFeed(array $settings, bool $manualRefresh): bool
    {
        $source = (string) ($settings['feed_source'] ?? 'wpvulnerability');
        if ($source === 'local_cache') {
            return false;
        }

        if ($manualRefresh) {
            return true;
        }

        $advisories = $this->loadAdvisories();
        if (!function_exists('get_option') && !empty($advisories)) {
            return false;
        }

        if (empty($advisories)) {
            return true;
        }

        if ((string) ($advisories['source'] ?? '') !== $source) {
            return true;
        }

        $updatedAt = strtotime((string) ($advisories['updated_at'] ?? ''));
        if (!$updatedAt) {
            return true;
        }

        return (time() - $updatedAt) > 12 * 3600;
    }

    /**
     * @param array<string,mixed> $settings
     * @param array<string,mixed> $inventory
     * @return array<string,mixed>
     */
    private function refreshAdvisoryFeed(array $settings, array $inventory = []): array
    {
        $source = (string) ($settings['feed_source'] ?? 'wpvulnerability');
        if ($source === 'local_cache') {
            return [
                'status' => 'skipped',
                'message' => 'Using local advisory cache.',
            ];
        }

        if ($source === 'wpvulnerability') {
            return $this->refreshWpvulnerabilityFeed($inventory);
        }

        $headers = [];
        $url = '';

        if ($source === 'custom_json') {
            $url = (string) ($settings['custom_feed_url'] ?? '');
            if ($url === '') {
                return [
                    'status' => 'failed',
                    'error' => 'Custom vulnerability feed URL is empty.',
                ];
            }
        } else {
            return [
                'status' => 'failed',
                'error' => 'Unknown vulnerability feed source.',
            ];
        }

        $response = $this->fetchJson($url, $headers);
        if (empty($response['ok']) || !isset($response['json']) || !is_array($response['json'])) {
            return [
                'status' => 'failed',
                'error' => (string) ($response['error'] ?? 'Feed request failed.'),
            ];
        }

        $normalized = $this->normalizeAdvisoryFeed($response['json'], $source);
        $normalized['updated_at'] = $this->nowMysql();
        $normalized['source'] = $source;
        $normalized['feed_url'] = $url;
        $this->writeAdvisories($normalized);

        return [
            'status' => 'refreshed',
            'records_total' => (int) ($normalized['records_total'] ?? 0),
            'source' => $source,
        ];
    }

    /**
     * @param array<string,mixed> $inventory
     * @return array<string,mixed>
     */
    private function refreshWpvulnerabilityFeed(array $inventory): array
    {
        if (empty($inventory)) {
            $inventory = $this->inventory();
        }

        $existing = $this->loadAdvisories();
        $existing = (string) ($existing['source'] ?? '') === 'wpvulnerability' ? $existing : [];
        $normalized = [
            'format' => 'shieldx-advisory-cache-v1',
            'source' => 'wpvulnerability',
            'updated_at' => (string) ($existing['updated_at'] ?? ''),
            'feed_url' => self::WPVULNERABILITY_ENDPOINT,
            'records_total' => 0,
            'core' => isset($existing['core']) && is_array($existing['core']) ? $existing['core'] : [],
            'plugins' => isset($existing['plugins']) && is_array($existing['plugins']) ? $existing['plugins'] : [],
            'themes' => isset($existing['themes']) && is_array($existing['themes']) ? $existing['themes'] : [],
            'software' => isset($existing['software']) && is_array($existing['software']) ? $existing['software'] : [],
            'errors' => [],
            'warnings' => [],
        ];
        $requestsLeft = self::MAX_WPVULNERABILITY_REQUESTS;

        $coreVersion = (string) ($inventory['core']['version'] ?? '');
        if ($coreVersion !== '') {
            $requestsLeft--;
            $payload = $this->fetchJson(self::WPVULNERABILITY_ENDPOINT . 'core/' . rawurlencode($coreVersion) . '/', []);
            if (!empty($payload['ok']) && isset($payload['json']) && is_array($payload['json'])) {
                $records = $this->normalizeWpvulnerabilityPayload($payload['json'], 'core', 'wordpress');
                $normalized['core'] = $records;
            } else {
                $normalized['errors'][] = 'core:' . (string) ($payload['error'] ?? 'request failed');
            }
        }

        $pluginQueue = [];
        foreach ((array) ($inventory['plugins'] ?? []) as $plugin) {
            if (!is_array($plugin)) {
                continue;
            }

            $slug = (string) ($plugin['slug'] ?? '');
            if ($slug === '') {
                continue;
            }

            $pluginQueue[] = $slug;
        }

        $themeQueue = [];
        foreach ((array) ($inventory['themes'] ?? []) as $theme) {
            if (!is_array($theme)) {
                continue;
            }

            $slug = (string) ($theme['slug'] ?? '');
            if ($slug === '') {
                continue;
            }

            $themeQueue[] = $slug;
        }

        $softwareQueue = [];
        foreach ((array) ($inventory['software'] ?? []) as $component) {
            if (!is_array($component)) {
                continue;
            }

            $slug = (string) ($component['slug'] ?? '');
            $version = (string) ($component['version'] ?? '');
            if ($slug === '' || $version === '' || !in_array($slug, self::SOFTWARE_COMPONENTS, true)) {
                continue;
            }

            $softwareQueue[] = $component;
        }

        while ($requestsLeft > 0 && (!empty($pluginQueue) || !empty($themeQueue) || !empty($softwareQueue))) {
            if (!empty($pluginQueue) && $requestsLeft > 0) {
                $requestsLeft--;
                $slug = (string) array_shift($pluginQueue);
                $payload = $this->fetchJson(self::WPVULNERABILITY_ENDPOINT . 'plugin/' . rawurlencode($slug) . '/', []);
                if (!empty($payload['ok']) && isset($payload['json']) && is_array($payload['json'])) {
                    $records = $this->normalizeWpvulnerabilityPayload($payload['json'], 'plugins', $slug);
                    if ($records) {
                        $normalized['plugins'][$slug] = $records;
                    }
                } else {
                    $normalized['errors'][] = 'plugin:' . $slug . ':' . (string) ($payload['error'] ?? 'request failed');
                }
            }

            if (!empty($themeQueue) && $requestsLeft > 0) {
                $requestsLeft--;
                $slug = (string) array_shift($themeQueue);
                $payload = $this->fetchJson(self::WPVULNERABILITY_ENDPOINT . 'theme/' . rawurlencode($slug) . '/', []);
                if (!empty($payload['ok']) && isset($payload['json']) && is_array($payload['json'])) {
                    $records = $this->normalizeWpvulnerabilityPayload($payload['json'], 'themes', $slug);
                    if ($records) {
                        $normalized['themes'][$slug] = $records;
                    }
                } else {
                    $normalized['errors'][] = 'theme:' . $slug . ':' . (string) ($payload['error'] ?? 'request failed');
                }
            }

            if (!empty($softwareQueue) && $requestsLeft > 0) {
                $requestsLeft--;
                $component = array_shift($softwareQueue);
                $component = is_array($component) ? $component : [];
                $slug = (string) ($component['slug'] ?? '');
                $version = (string) ($component['version'] ?? '');
                $payload = $this->fetchJson(self::WPVULNERABILITY_ENDPOINT . rawurlencode($slug) . '/' . rawurlencode($version) . '/', []);
                if (!empty($payload['ok']) && isset($payload['json']) && is_array($payload['json'])) {
                    $records = $this->normalizeWpvulnerabilityPayload($payload['json'], 'software', $slug);
                    if ($records) {
                        $normalized['software'][$slug] = $records;
                    }
                } else {
                    $normalized['warnings'][] = $this->softwareFeedWarning($component, (string) ($payload['error'] ?? 'request failed'));
                }
            }
        }

        if (!empty($pluginQueue)) {
            $normalized['errors'][] = 'request_budget:plugin refresh deferred';
        }
        if (!empty($themeQueue)) {
            $normalized['errors'][] = 'request_budget:theme refresh deferred';
        }
        if (!empty($softwareQueue)) {
            $normalized['warnings'][] = 'request_budget:software refresh deferred';
        }

        $normalized['records_total'] = $this->countNormalizedAdvisories($normalized);
        $hasErrors = !empty($normalized['errors']);
        $hasWarnings = !empty($normalized['warnings']);
        if (!$hasErrors) {
            $normalized['updated_at'] = $this->nowMysql();
        }
        $this->writeAdvisories($normalized);

        return [
            'status' => $hasErrors ? 'refreshed_with_errors' : ($hasWarnings ? 'refreshed_with_warnings' : 'refreshed'),
            'records_total' => (int) $normalized['records_total'],
            'source' => 'wpvulnerability',
            'error' => $hasErrors ? implode('; ', array_slice(array_map('strval', $normalized['errors']), 0, 3)) : '',
            'warning' => $hasWarnings ? implode('; ', array_slice(array_map('strval', $normalized['warnings']), 0, 3)) : '',
        ];
    }

    /**
     * @param array<string,mixed> $component
     */
    private function softwareFeedWarning(array $component, string $error): string
    {
        $name = (string) ($component['name'] ?? $component['slug'] ?? 'software');
        $version = (string) ($component['version'] ?? '');
        $label = trim($name . ($version !== '' ? ' ' . $version : ''));

        return sprintf(
            'Optional runtime advisory feed skipped for %s: %s',
            $label !== '' ? $label : 'software',
            $error
        );
    }

    /**
     * @param array<string,mixed> $raw
     * @return array<int,array<string,mixed>>
     */
    private function normalizeWpvulnerabilityPayload(array $raw, string $bucket, string $slug): array
    {
        if (!empty($raw['error'])) {
            return [];
        }

        $data = isset($raw['data']) && is_array($raw['data']) ? $raw['data'] : $raw;
        $records = [];
        foreach (['vulnerability', 'vulnerabilities'] as $key) {
            if (isset($data[$key]) && is_array($data[$key])) {
                $records = $data[$key];
                break;
            }
        }

        if (empty($records) && array_keys($data) === range(0, count($data) - 1)) {
            $records = $data;
        }

        $items = [];

        foreach ($records as $record) {
            if (is_array($record)) {
                $items[] = $this->normalizeWpvulnerabilityRecord($record, $bucket, $slug);
            }
        }

        return $items;
    }

    /**
     * @param array<string,mixed> $record
     * @return array<string,mixed>
     */
    private function normalizeWpvulnerabilityRecord(array $record, string $bucket, string $slug): array
    {
        $sources = isset($record['source']) && is_array($record['source']) ? $record['source'] : [];
        $references = [];
        $sourceDescriptions = [];
        $cve = '';

        foreach ($sources as $source) {
            if (!is_array($source)) {
                continue;
            }

            $link = (string) ($source['link'] ?? '');
            if ($link !== '') {
                $references[] = $link;
            }

            $sourceId = (string) ($source['id'] ?? $source['name'] ?? '');
            if ($cve === '' && preg_match('/CVE-\d{4}-\d{4,}/i', $sourceId, $matches) === 1) {
                $cve = strtoupper((string) $matches[0]);
            }

            $description = (string) ($source['description'] ?? '');
            if ($description !== '') {
                $sourceDescriptions[] = $description;
            }
        }

        $impact = isset($record['impact']) && is_array($record['impact']) ? $record['impact'] : [];
        $ssvc = isset($impact['ssvc']) && is_array($impact['ssvc']) ? $impact['ssvc'] : [];
        $severity = $this->severityFromWpvulnerabilityImpact($impact);
        $operator = isset($record['operator']) && is_array($record['operator']) ? $record['operator'] : [];
        if (empty($operator) && isset($record['unfixed'])) {
            $operator['unfixed'] = (int) $record['unfixed'];
        }
        $unfixed = !empty($operator['unfixed']) && (string) $operator['unfixed'] !== '0';
        $closed = !empty($operator['closed']) && (string) $operator['closed'] !== '0';
        $affected = (string) ($record['versions'] ?? $record['affected'] ?? '');
        $version = (string) ($record['version'] ?? '');
        $epss = $impact['epss'] ?? null;
        if (is_array($epss)) {
            $epss = $epss['score'] ?? $epss['percentile'] ?? null;
        }
        $exploitation = strtolower((string) ($ssvc['exploitation'] ?? $impact['exploitation'] ?? ''));
        $automatable = strtolower((string) ($ssvc['automatable'] ?? $impact['automatable'] ?? ''));
        $kev = !empty($impact['kev']) || !empty($ssvc['kev']);

        return [
            'id' => (string) ($record['uuid'] ?? sha1($bucket . '|' . $slug . '|' . (string) ($record['name'] ?? ''))),
            'title' => (string) ($record['name'] ?? 'Known vulnerability advisory'),
            'description' => (string) ($record['description'] ?? ''),
            'severity' => $severity,
            'score' => $this->scoreFromWpvulnerabilityImpact($impact),
            'cve' => $cve,
            'cwe' => $this->cweFromWpvulnerabilityImpact($impact),
            'references' => array_values(array_unique($references)),
            'source_descriptions' => array_values(array_unique($sourceDescriptions)),
            'wpvulnerability_operator' => $operator,
            'version_specific' => $bucket === 'core',
            'affected' => $affected,
            'reported_version' => $version,
            'kev' => $kev,
            'epss' => is_numeric($epss) ? (float) $epss : null,
            'exploitation' => $exploitation,
            'automatable' => $automatable,
            'patched' => !$unfixed,
            'fixed_in' => (string) ($operator['max_version'] ?? '') !== '' && !$unfixed ? [(string) $operator['max_version']] : [],
            'remediation' => $closed
                ? 'Component appears closed; disable or replace it.'
                : ($unfixed ? 'No fixed version is marked. Disable the component or keep WAF virtual patching enabled until a fixed release exists.' : 'Update beyond the affected version range.'),
            'source' => 'WPVulnerability',
        ];
    }

    /**
     * @param array<string,mixed> $impact
     */
    private function severityFromWpvulnerabilityImpact(array $impact): string
    {
        foreach (['cvss4', 'cvss3', 'cvss2', 'cvss'] as $key) {
            if (!isset($impact[$key]) || !is_array($impact[$key])) {
                continue;
            }

            $severity = strtolower((string) ($impact[$key]['severity'] ?? ''));
            if ($severity === 'c') {
                return 'critical';
            }

            if (in_array($severity, ['critical', 'high', 'medium', 'low'], true)) {
                return $severity;
            }
        }

        return 'medium';
    }

    /**
     * @param array<string,mixed> $impact
     */
    private function scoreFromWpvulnerabilityImpact(array $impact): float
    {
        foreach (['cvss4', 'cvss3', 'cvss2', 'cvss'] as $key) {
            if (isset($impact[$key]) && is_array($impact[$key]) && isset($impact[$key]['score'])) {
                return (float) $impact[$key]['score'];
            }
        }

        return 0.0;
    }

    /**
     * @param array<string,mixed> $impact
     */
    private function cweFromWpvulnerabilityImpact(array $impact): string
    {
        $cwe = isset($impact['cwe']) && is_array($impact['cwe']) ? reset($impact['cwe']) : null;
        if (is_array($cwe)) {
            return (string) ($cwe['cwe'] ?? '');
        }

        return '';
    }

    /**
     * @param array<string,string> $headers
     * @return array<string,mixed>
     */
    private function fetchJson(string $url, array $headers): array
    {
        if (!function_exists('wp_remote_get')) {
            return ['ok' => false, 'error' => 'WordPress HTTP API is not available.'];
        }

        if (!self::isSafeFeedUrl($url)) {
            return ['ok' => false, 'error' => 'Feed URL is not an allowed public HTTPS endpoint.'];
        }

        $pinnedIp = self::resolvePublicFeedIp($url);
        if ($pinnedIp === '' || !self::isPublicIp($pinnedIp)) {
            return ['ok' => false, 'error' => 'Feed host could not be resolved to a public IP address.'];
        }

        $curlPin = null;
        if (function_exists('add_action') && function_exists('remove_action')) {
            $curlPin = static function ($handle, $requestArgs, $requestUrl) use ($url, $pinnedIp): void {
                if ((string) $requestUrl !== $url || !function_exists('curl_setopt') || !defined('CURLOPT_RESOLVE')) {
                    return;
                }

                $parts = self::urlParts($url);
                if (!is_array($parts)) {
                    return;
                }

                $host = (string) ($parts['host'] ?? '');
                $port = (int) ($parts['port'] ?? 443);
                // phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt -- The request still uses wp_remote_get(); this hook only pins the previously validated public IP because WP HTTP has no CURLOPT_RESOLVE abstraction.
                curl_setopt($handle, CURLOPT_RESOLVE, [$host . ':' . (string) $port . ':' . $pinnedIp]);
            };
            try {
                add_action('http_api_curl', $curlPin, 10, 3);
            } catch (\Throwable $error) {
                return ['ok' => false, 'error' => 'Feed request pinning failed: ' . $error->getMessage()];
            }
        }

        try {
            $response = wp_remote_get($url, [
                'headers' => $headers,
                'timeout' => 20,
                'redirection' => 0,
                'reject_unsafe_urls' => true,
                'limit_response_size' => self::MAX_FEED_RESPONSE_BYTES,
            ]);
        } catch (\Throwable $error) {
            return ['ok' => false, 'error' => $error->getMessage()];
        } finally {
            if ($curlPin !== null && function_exists('remove_action')) {
                try {
                    remove_action('http_api_curl', $curlPin, 10);
                } catch (\Throwable $error) {
                    \ShieldX\Support\ErrorLog::write('SHIELD-X vulnerability feed curl-pin cleanup failed: ' . $error->getMessage());
                }
            }
        }

        if (function_exists('is_wp_error') && is_wp_error($response)) {
            return ['ok' => false, 'error' => $response->get_error_message()];
        }

        $code = function_exists('wp_remote_retrieve_response_code') ? (int) wp_remote_retrieve_response_code($response) : 0;
        $body = function_exists('wp_remote_retrieve_body') ? (string) wp_remote_retrieve_body($response) : '';
        if ($code < 200 || $code >= 300 || $body === '') {
            return ['ok' => false, 'error' => 'Feed returned HTTP ' . (string) $code . '.'];
        }

        $decoded = \ShieldX\Support\Json::decodeArray($body);

        if (is_array($decoded)) {
            return ['ok' => true, 'json' => $decoded];
        }

        if (strlen($body) >= self::MAX_FEED_RESPONSE_BYTES) {
            return ['ok' => false, 'error' => 'Feed response exceeded the local size limit before complete JSON was received.'];
        }

        return ['ok' => false, 'error' => 'Feed response was not valid JSON.'];
    }

    private static function resolvePublicFeedIp(string $url): string
    {
        $parts = self::urlParts($url);
        if (!is_array($parts)) {
            return '';
        }

        $host = strtolower(trim((string) ($parts['host'] ?? ''), "[] \t\n\r\0\x0B"));
        if ($host === '') {
            return '';
        }

        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return self::isPublicIp($host) ? $host : '';
        }

        $addresses = self::publicFeedHostAddresses($host);
        if (!is_array($addresses) || empty($addresses)) {
            return '';
        }

        foreach ($addresses as $address) {
            $address = (string) $address;
            if (!self::isPublicIp($address)) {
                return '';
            }
        }

        return (string) $addresses[0];
    }

    /**
     * @return array<int,string>|null
     */
    private static function publicFeedHostAddresses(string $host): ?array
    {
        if (!function_exists('gethostbynamel')) {
            return null;
        }

        $cacheKey = 'shieldx_feed_dns_' . hash('sha256', strtolower($host));
        if (function_exists('get_transient')) {
            try {
                $cached = get_transient($cacheKey);
                if (is_array($cached) && isset($cached['addresses']) && is_array($cached['addresses'])) {
                    return array_values(array_filter(array_map('strval', $cached['addresses'])));
                }
            } catch (\Throwable $error) {
                \ShieldX\Support\ErrorLog::write('SHIELD-X vulnerability feed DNS cache read failed: ' . $error->getMessage());
            }
        }

        $addresses = gethostbynamel($host);
        $addresses = is_array($addresses) ? array_values(array_filter(array_map('strval', $addresses))) : [];

        if (function_exists('set_transient')) {
            try {
                set_transient($cacheKey, ['addresses' => $addresses], self::FEED_DNS_CACHE_SECONDS);
            } catch (\Throwable $error) {
                \ShieldX\Support\ErrorLog::write('SHIELD-X vulnerability feed DNS cache write failed: ' . $error->getMessage());
            }
        }

        return $addresses;
    }

    /**
     * @param array<string,mixed> $raw
     * @return array<string,mixed>
     */
    private function normalizeAdvisoryFeed(array $raw, string $source): array
    {
        if (isset($raw['plugins']) || isset($raw['themes']) || isset($raw['core']) || isset($raw['software'])) {
            return array_merge([
                'format' => 'shieldx-advisory-cache-v1',
                'records_total' => $this->countNormalizedAdvisories($raw),
                'source' => $source,
                'core' => [],
                'plugins' => [],
                'themes' => [],
                'software' => [],
            ], $raw);
        }

        $normalized = [
            'format' => 'shieldx-advisory-cache-v1',
            'source' => $source,
            'records_total' => 0,
            'core' => [],
            'plugins' => [],
            'themes' => [],
            'software' => [],
        ];

        foreach ($raw as $record) {
            if (!is_array($record)) {
                continue;
            }

            $software = isset($record['software']) && is_array($record['software']) ? $record['software'] : [];
            foreach ($software as $component) {
                if (!is_array($component)) {
                    continue;
                }

                $type = strtolower((string) ($component['type'] ?? ''));
                $slug = (string) ($component['slug'] ?? '');
                $advisory = $this->normalizeExternalAdvisoryRecord($record, $component);
                if ($type === 'core') {
                    $normalized['core'][] = $advisory;
                    $normalized['records_total']++;
                } elseif ($type === 'plugin' && $slug !== '') {
                    if (!isset($normalized['plugins'][$slug])) {
                        $normalized['plugins'][$slug] = [];
                    }
                    $normalized['plugins'][$slug][] = $advisory;
                    $normalized['records_total']++;
                } elseif ($type === 'theme' && $slug !== '') {
                    if (!isset($normalized['themes'][$slug])) {
                        $normalized['themes'][$slug] = [];
                    }
                    $normalized['themes'][$slug][] = $advisory;
                    $normalized['records_total']++;
                } elseif ($type === 'software' && $slug !== '') {
                    if (!isset($normalized['software'][$slug])) {
                        $normalized['software'][$slug] = [];
                    }
                    $normalized['software'][$slug][] = $advisory;
                    $normalized['records_total']++;
                }
            }
        }

        return $normalized;
    }

    /**
     * @param array<string,mixed> $record
     * @param array<string,mixed> $component
     * @return array<string,mixed>
     */
    private function normalizeExternalAdvisoryRecord(array $record, array $component): array
    {
        $cvss = isset($record['cvss']) && is_array($record['cvss']) ? $record['cvss'] : [];
        $cwe = isset($record['cwe']) && is_array($record['cwe']) ? $record['cwe'] : [];

        return [
            'id' => (string) ($record['id'] ?? ''),
            'title' => (string) ($record['title'] ?? 'Known vulnerability advisory'),
            'severity' => (string) ($cvss['rating'] ?? 'high'),
            'score' => (float) ($cvss['score'] ?? 0),
            'cve' => (string) ($record['cve'] ?? ''),
            'cwe' => !empty($cwe['id']) ? 'CWE-' . (string) $cwe['id'] : '',
            'references' => isset($record['references']) && is_array($record['references']) ? array_values(array_map('strval', $record['references'])) : [],
            'affected_versions' => isset($component['affected_versions']) && is_array($component['affected_versions']) ? $component['affected_versions'] : [],
            'fixed_in' => isset($component['patched_versions']) && is_array($component['patched_versions']) ? array_values(array_map('strval', $component['patched_versions'])) : [],
            'patched' => !empty($component['patched']),
            'remediation' => (string) ($component['remediation'] ?? ''),
            'published' => (string) ($record['published'] ?? ''),
            'source' => (string) ($record['source'] ?? 'Custom advisory feed'),
        ];
    }

    /**
     * @param array<string,mixed> $raw
     */
    private function countNormalizedAdvisories(array $raw): int
    {
        $count = count((array) ($raw['core'] ?? []));
        foreach (['plugins', 'themes', 'software'] as $bucket) {
            foreach ((array) ($raw[$bucket] ?? []) as $items) {
                $count += is_array($items) ? count($items) : 0;
            }
        }

        return $count;
    }

    /**
     * @return array<string,mixed>
     */
    private function feedStatus(): array
    {
        $advisories = $this->loadAdvisories();

        return [
            'status' => !empty($advisories) ? 'cached' : 'empty',
            'records_total' => (int) ($advisories['records_total'] ?? 0),
            'source' => (string) ($advisories['source'] ?? 'local-cache'),
            'error' => !empty($advisories['errors']) && is_array($advisories['errors'])
                ? implode('; ', array_slice(array_map('strval', $advisories['errors']), 0, 3))
                : '',
            'warning' => !empty($advisories['warnings']) && is_array($advisories['warnings'])
                ? implode('; ', array_slice(array_map('strval', $advisories['warnings']), 0, 3))
                : '',
        ];
    }

    /**
     * @param array<string,mixed> $settings
     * @return array<string,mixed>
     */
    private function publicFeedSettings(array $settings): array
    {
        return [
            'feed_source' => (string) ($settings['feed_source'] ?? 'wpvulnerability'),
            'custom_feed_url' => (string) ($settings['custom_feed_url'] ?? ''),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function readJson(string $path): array
    {
        if (!is_file($path) || !is_readable($path)) {
            return [];
        }

        $raw = file_get_contents($path);
        $decoded = is_string($raw) ? \ShieldX\Support\Json::decodeArray($raw) : null;

        return is_array($decoded) ? $decoded : [];
    }

    /**
     * @return array<string,mixed>
     */
    private function readSignedAdvisories(string $path): array
    {
        $decoded = $this->readJson($path);
        if (empty($decoded)) {
            return [];
        }

        return $this->advisorySignatureValid($decoded) ? $decoded : [];
    }

    /**
     * @param array<string,mixed> $data
     */
    private function writeAdvisories(array $data): void
    {
        $data['signature'] = [
            'alg' => 'hmac-sha256',
            'value' => $this->signAdvisories($data),
        ];
        $this->writeJson($this->advisoryPath(), $data);
    }

    /**
     * @param array<string,mixed> $data
     */
    private function advisorySignatureValid(array $data): bool
    {
        $signature = isset($data['signature']) && is_array($data['signature']) ? $data['signature'] : [];
        $algorithm = (string) ($signature['alg'] ?? '');
        $value = (string) ($signature['value'] ?? '');
        if ($algorithm !== 'hmac-sha256' || preg_match('/^[a-f0-9]{64}$/i', $value) !== 1) {
            return false;
        }

        return hash_equals($this->signAdvisories($data), $value);
    }

    /**
     * @param array<string,mixed> $data
     */
    private function signAdvisories(array $data): string
    {
        unset($data['signature']);
        self::sortRecursive($data);
        $json = wp_json_encode($data, JSON_UNESCAPED_SLASHES);
        $salt = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $key = hash('sha256', $salt . '|shieldx-vulnerability-advisory-cache|' . $this->storage->baseDir(), true);

        return is_string($json) ? hash_hmac('sha256', $json, $key) : '';
    }

    /**
     * @param array<mixed> $data
     */
    private static function sortRecursive(array &$data): void
    {
        ksort($data);
        foreach ($data as &$value) {
            if (is_array($value)) {
                self::sortRecursive($value);
            }
        }
        unset($value);
    }

    /**
     * @param array<string,mixed> $data
     */
    private function writeJson(string $path, array $data): void
    {
        $dir = dirname($path);
        if (!is_dir($dir)) {
            if (function_exists('wp_mkdir_p')) {
                wp_mkdir_p($dir);
            }
        }

        $json = wp_json_encode($data);
        if (!is_string($json)) {
            return;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_tempnam -- Private plugin cache writes need an atomic temp file in the same directory; no host credential prompt is appropriate.
        $tmp = tempnam($dir, 'shieldx-json-');
        if (!is_string($tmp) || $tmp === '') {
            return;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- This writes SHIELD-X's private advisory cache, not a host-managed WordPress file.
        if (file_put_contents($tmp, $json, LOCK_EX) === false) {
            if (file_exists($tmp)) {
                // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Clean up the failed temporary cache file created above.
                unlink($tmp);
            }
            return;
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Atomic same-directory replacement keeps advisory cache readers from seeing partial JSON.
        if (!rename($tmp, $path) && file_exists($tmp)) {
            // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Clean up the temporary cache file if the atomic replace failed.
            unlink($tmp);
        }
    }

    private function statePath(): string
    {
        return $this->storage->baseDir() . '/cache/' . self::STATE_FILE;
    }

    private function advisoryPath(): string
    {
        return $this->storage->baseDir() . '/cache/' . self::ADVISORY_FILE;
    }

    private function nowMysql(): string
    {
        return function_exists('current_time') ? (string) current_time('mysql', true) : gmdate('Y-m-d H:i:s');
    }

    /**
     * @param array<int,array<string,mixed>> $issues
     */
    private function countUpdateIssues(array $issues): int
    {
        $count = 0;

        foreach ($issues as $issue) {
            $rule = (string) ($issue['rule_id'] ?? '');
            if (in_array($rule, ['SX-VULN-CORE-UPDATE', 'SX-VULN-PLUGIN-UPDATE', 'SX-VULN-THEME-UPDATE'], true)) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * @param array<int,array<string,mixed>> $issues
     */
    private function countIssuesByRule(array $issues, string $ruleId): int
    {
        $count = 0;

        foreach ($issues as $issue) {
            if ((string) ($issue['rule_id'] ?? '') === $ruleId) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * @param array<int,array<string,mixed>> $issues
     */
    private function countIssuesByRuleAndPathPrefix(array $issues, string $ruleId, string $prefix): int
    {
        $count = 0;

        foreach ($issues as $issue) {
            if ((string) ($issue['rule_id'] ?? '') === $ruleId && strpos((string) ($issue['affected_path'] ?? ''), $prefix) === 0) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * @param array<int,array<string,mixed>> $software
     */
    private function countDetectedSoftware(array $software): int
    {
        $count = 0;

        foreach ($software as $component) {
            if (is_array($component) && !empty($component['detected']) && (string) ($component['version'] ?? '') !== '') {
                $count++;
            }
        }

        return $count;
    }

    /**
     * @param array<int,array<string,mixed>> $software
     * @return array<int,array<string,mixed>>
     */
    private function publicSoftwareInventory(array $software): array
    {
        $items = [];

        foreach ($software as $component) {
            if (!is_array($component)) {
                continue;
            }

            $slug = (string) ($component['slug'] ?? '');
            if ($slug === '') {
                continue;
            }

            $items[] = [
                'slug' => $slug,
                'name' => (string) ($component['name'] ?? $slug),
                'version' => (string) ($component['version'] ?? ''),
                'method' => (string) ($component['method'] ?? ''),
                'reliability' => max(0, min(100, (int) ($component['reliability'] ?? 0))),
                'detected' => !empty($component['detected']) && (string) ($component['version'] ?? '') !== '',
            ];
        }

        return $items;
    }

    /**
     * @param array<string,mixed> $advisory
     * @return array<int,string>
     */
    private function advisoryBadges(array $advisory): array
    {
        $badges = [];
        $score = (float) ($advisory['score'] ?? 0);
        if ($score > 0) {
            $badges[] = 'CVSS ' . number_format($score, 1, '.', '');
        }

        $cve = (string) ($advisory['cve'] ?? '');
        if ($cve !== '') {
            $badges[] = $cve;
        }

        if (!empty($advisory['kev'])) {
            $badges[] = 'actively exploited';
        }

        $exploitation = strtolower((string) ($advisory['exploitation'] ?? ''));
        if ($exploitation === 'poc') {
            $badges[] = 'public exploit';
        } elseif ($exploitation === 'active') {
            $badges[] = 'active exploitation';
        }

        if (strtolower((string) ($advisory['automatable'] ?? '')) === 'yes') {
            $badges[] = 'automatable';
        }

        $epss = $advisory['epss'] ?? null;
        if (is_numeric($epss)) {
            $badges[] = 'EPSS ' . number_format((float) $epss, 3, '.', '');
        }

        $references = isset($advisory['references']) && is_array($advisory['references']) ? count($advisory['references']) : 0;
        if ($references > 0) {
            $badges[] = sprintf('%d reference%s', $references, $references === 1 ? '' : 's');
        }

        return array_values(array_unique($badges));
    }

    private function normalizeSeverity(string $severity): string
    {
        $severity = strtolower(trim($severity));

        return in_array($severity, ['critical', 'high', 'medium', 'low'], true) ? $severity : 'medium';
    }

    private function majorMinor(string $version): string
    {
        if (preg_match('/^(\d+)(?:\.(\d+))?/', $version, $matches) !== 1) {
            return $version;
        }

        return (string) $matches[1] . '.' . (string) ($matches[2] ?? '0');
    }

    private function versionGap(string $current, string $new): string
    {
        $currentParts = $this->versionParts($current);
        $newParts = $this->versionParts($new);

        if ($currentParts[0] !== $newParts[0]) {
            return 'major';
        }

        if (($newParts[1] - $currentParts[1]) >= 2) {
            return 'minor';
        }

        return '';
    }

    /**
     * @return array{0:int,1:int,2:int}
     */
    private function versionParts(string $version): array
    {
        preg_match('/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/', $version, $matches);

        return [
            isset($matches[1]) ? (int) $matches[1] : 0,
            isset($matches[2]) ? (int) $matches[2] : 0,
            isset($matches[3]) ? (int) $matches[3] : 0,
        ];
    }

    public static function slugFromPluginFile(string $pluginFile): string
    {
        $pluginFile = trim(str_replace('\\', '/', $pluginFile), '/');
        if ($pluginFile === '') {
            return '';
        }

        $dir = dirname($pluginFile);
        if ($dir !== '.' && $dir !== '') {
            return self::sanitizeSlug($dir);
        }

        return self::sanitizeSlug((string) preg_replace('/\.php$/i', '', basename($pluginFile)));
    }

    private static function sanitizeSlug(string $value): string
    {
        if (function_exists('sanitize_key')) {
            return sanitize_key($value);
        }

        $value = strtolower($value);
        $value = preg_replace('/[^a-z0-9_\-]/', '-', $value);

        return is_string($value) ? trim($value, '-') : '';
    }

    public static function isStaleLastUpdated(string $lastUpdated, ?int $nowTimestamp = null, int $maxAgeDays = self::STALE_PLUGIN_DAYS): bool
    {
        $timestamp = strtotime($lastUpdated);
        if (!$timestamp) {
            return false;
        }

        $nowTimestamp = $nowTimestamp ?: time();

        return ($nowTimestamp - $timestamp) > ($maxAgeDays * 86400);
    }

    /**
     * @param array<string,mixed> $advisory
     */
    public static function advisoryAffectsVersion(array $advisory, string $version): bool
    {
        $version = trim($version);
        if ($version === '') {
            return false;
        }

        if (!empty($advisory['version_specific'])) {
            return true;
        }

        if (isset($advisory['wpvulnerability_operator']) && is_array($advisory['wpvulnerability_operator']) && !empty($advisory['wpvulnerability_operator'])) {
            return self::wpvulnerabilityOperatorAffects($version, $advisory['wpvulnerability_operator']);
        }

        if (isset($advisory['affected_versions']) && is_array($advisory['affected_versions'])) {
            foreach ($advisory['affected_versions'] as $constraint => $range) {
                if (is_array($range) && self::versionRangeAffects($version, $range)) {
                    return true;
                }

                if (is_string($constraint) && self::versionSatisfies($version, $constraint)) {
                    return true;
                }
            }
        }

        if (!self::hasAffectedVersionConstraints($advisory)) {
            foreach (['fixed_in', 'patched_in'] as $key) {
                if (isset($advisory[$key]) && self::fixedVersionsAffect($version, (array) $advisory[$key])) {
                    return true;
                }
            }
        }

        foreach (['affected_versions', 'affected', 'vulnerable_versions'] as $key) {
            if (!isset($advisory[$key])) {
                continue;
            }

            foreach ((array) $advisory[$key] as $constraint) {
                if (!is_scalar($constraint)) {
                    continue;
                }
                if (self::versionSatisfies($version, (string) $constraint)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * @param array<string,mixed> $advisory
     */
    private static function hasAffectedVersionConstraints(array $advisory): bool
    {
        foreach (['affected_versions', 'affected', 'vulnerable_versions', 'versions'] as $key) {
            if (!empty($advisory[$key])) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param array<int,mixed> $fixedVersions
     */
    private static function fixedVersionsAffect(string $version, array $fixedVersions): bool
    {
        $sameMajorGreaterFixed = [];
        $greaterFixed = [];
        $hasSameMajorFixed = false;
        foreach ($fixedVersions as $fixedVersion) {
            if (!is_scalar($fixedVersion)) {
                continue;
            }
            $fixedVersion = trim((string) $fixedVersion);
            if ($fixedVersion === '') {
                continue;
            }

            if (self::majorVersion($version) === self::majorVersion($fixedVersion)) {
                $hasSameMajorFixed = true;
            }

            if (!version_compare($version, $fixedVersion, '<')) {
                continue;
            }

            $greaterFixed[] = $fixedVersion;
            if (self::majorVersion($version) === self::majorVersion($fixedVersion)) {
                $sameMajorGreaterFixed[] = $fixedVersion;
            }
        }

        return $hasSameMajorFixed ? !empty($sameMajorGreaterFixed) : !empty($greaterFixed);
    }

    private static function majorVersion(string $version): string
    {
        return preg_match('/^v?(\d+)/i', trim($version), $matches) === 1 ? (string) $matches[1] : '';
    }

    /**
     * @param array<string,mixed> $range
     */
    private static function versionRangeAffects(string $version, array $range): bool
    {
        $from = (string) ($range['from_version'] ?? '*');
        $to = (string) ($range['to_version'] ?? '*');
        $fromInclusive = array_key_exists('from_inclusive', $range) ? !empty($range['from_inclusive']) : true;
        $toInclusive = array_key_exists('to_inclusive', $range) ? !empty($range['to_inclusive']) : true;

        if ($from !== '*' && !version_compare($version, $from, $fromInclusive ? '>=' : '>')) {
            return false;
        }

        if ($to !== '*' && !version_compare($version, $to, $toInclusive ? '<=' : '<')) {
            return false;
        }

        return true;
    }

    /**
     * @param array<string,mixed> $operator
     */
    private static function wpvulnerabilityOperatorAffects(string $version, array $operator): bool
    {
        $minVersion = trim((string) ($operator['min_version'] ?? ''));
        $maxVersion = trim((string) ($operator['max_version'] ?? ''));
        $minOperator = self::normalizeVersionOperator((string) ($operator['min_operator'] ?? 'ge'));
        $maxOperator = self::normalizeVersionOperator((string) ($operator['max_operator'] ?? 'le'));

        if ($minVersion !== '' && !version_compare($version, $minVersion, $minOperator)) {
            return false;
        }

        if ($maxVersion !== '' && !version_compare($version, $maxVersion, $maxOperator)) {
            return false;
        }

        return $minVersion !== '' || $maxVersion !== '' || !empty($operator['unfixed']);
    }

    private static function normalizeVersionOperator(string $operator): string
    {
        $operator = strtolower(trim($operator));
        $map = [
            'lt' => '<',
            'le' => '<=',
            'lte' => '<=',
            'gt' => '>',
            'ge' => '>=',
            'gte' => '>=',
            'eq' => '=',
            '==' => '=',
            '=' => '=',
            '<' => '<',
            '<=' => '<=',
            '>' => '>',
            '>=' => '>=',
        ];

        return isset($map[$operator]) ? $map[$operator] : '=';
    }

    public static function versionSatisfies(string $version, string $constraint): bool
    {
        $version = trim($version);
        $constraint = trim($constraint);

        if ($version === '' || $constraint === '') {
            return false;
        }

        if ($constraint === '*' || strtolower($constraint) === 'all') {
            return true;
        }

        $orGroups = preg_split('/\s*\|\|\s*/', $constraint);
        if (!is_array($orGroups)) {
            return false;
        }

        foreach ($orGroups as $group) {
            if (self::versionSatisfiesAll($version, (string) $group)) {
                return true;
            }
        }

        return false;
    }

    private static function versionSatisfiesAll(string $version, string $constraint): bool
    {
        $constraint = trim($constraint);
        if ($constraint === '') {
            return false;
        }

        if (preg_match('/^(?:=|==)?\s*v?([0-9]+(?:\.[0-9]+)*\.(?:x|\*))$/i', $constraint, $wildcard) === 1) {
            return self::versionMatchesWildcard($version, (string) $wildcard[1]);
        }

        if (preg_match('/^v?([0-9][0-9A-Za-z.\-+_]*)\s*-\s*v?([0-9][0-9A-Za-z.\-+_]*)$/', $constraint, $range) === 1) {
            return version_compare($version, $range[1], '>=') && version_compare($version, $range[2], '<=');
        }

        preg_match_all('/(<=|>=|<|>|=|==)?\s*v?([0-9][0-9A-Za-z.\-+_]*)/', $constraint, $matches, PREG_SET_ORDER);
        if (empty($matches)) {
            return false;
        }

        foreach ($matches as $match) {
            $operator = (string) ($match[1] ?: '=');
            if ($operator === '==') {
                $operator = '=';
            }

            if (!version_compare($version, (string) $match[2], $operator)) {
                return false;
            }
        }

        return true;
    }

    private static function versionMatchesWildcard(string $version, string $constraint): bool
    {
        $parts = preg_split('/\./', strtolower($constraint));
        if (!is_array($parts)) {
            return false;
        }

        $prefix = [];
        foreach ($parts as $part) {
            if ($part === 'x' || $part === '*') {
                break;
            }
            $prefix[] = $part;
        }

        if (empty($prefix)) {
            return true;
        }

        $lowerParts = $prefix;
        while (count($lowerParts) < 3) {
            $lowerParts[] = '0';
        }
        $upperParts = $lowerParts;
        $bumpIndex = max(0, count($prefix) - 1);
        $upperParts[$bumpIndex] = (string) ((int) $upperParts[$bumpIndex] + 1);
        for ($index = $bumpIndex + 1; $index < count($upperParts); $index++) {
            $upperParts[$index] = '0';
        }

        return version_compare($version, implode('.', $lowerParts), '>=') && version_compare($version, implode('.', $upperParts), '<');
    }
}
