<?php

namespace CleantalkSP\SpbctWP\VulnerabilityAlarm;

use CleantalkSP\SpbctWP\VulnerabilityAlarm\Dto\ApiResults;
use CleantalkSP\SpbctWP\VulnerabilityAlarm\Dto\PluginReport;
use WP_Error;

class PscCertifiedPluginsCache
{
    const CATALOG_PREFETCH_COUNT = 20;

    const CACHE_TTL = 86400;

    /**
     * Warm installed and catalog PSC caches during activation or sync.
     *
     * Fetches ALL PSC items from the Research API in a single request,
     * enriches the first CATALOG_PREFETCH_COUNT with WordPress.org data,
     * and stores the rest with badge data only (WP data is fetched on demand).
     *
     * @return void
     */
    public static function warm()
    {
        self::ensureTableExists();

        $installed_rows = self::buildInstalledRows();
        $installed_slugs = [];

        foreach ($installed_rows as $row) {
            $installed_slugs[$row['slug']] = true;
        }

        $api_result = PscCertifiedPluginsApi::fetchAll();

        $catalog_rows = [];
        $catalog_total = 0;

        if (! ($api_result instanceof WP_Error)) {
            $catalog_total = (int) $api_result['total'];

            require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

            $psc_items = PscCertifiedPluginsCatalogScanner::filterPscCatalogItems($api_result['items']);
            $all_badges = PscCertifiedPluginsCatalogScanner::buildBadgesFromCatalogItems($psc_items);

            $sort_order = 0;
            $prefetched = 0;

            foreach ($psc_items as $item) {
                $slug = sanitize_title((string) ($item['slug'] ?? ''));

                if (empty($slug) || isset($installed_slugs[$slug]) || empty($all_badges[$slug])) {
                    continue;
                }

                $plugin_data = '';

                if ($prefetched < self::CATALOG_PREFETCH_COUNT) {
                    $plugin = PscCertifiedPluginsCatalogScanner::fetchPluginFromCatalogItem($item);

                    if ($plugin) {
                        $report = $all_badges[$slug]['report_object'] ?? null;
                        $wp_version = (string) ($plugin['version'] ?? '');

                        if ($report instanceof PluginReport && ! self::isVersionInPscRange($report, $wp_version)) {
                            continue;
                        }

                        $plugin_data = self::encodePlugin($plugin);
                        $prefetched++;
                    }
                }

                $catalog_rows[] = [
                    'slug'        => $slug,
                    'source'      => PscCertifiedPluginsCacheRepository::SOURCE_CATALOG,
                    'sort_order'  => $sort_order++,
                    'plugin_data' => $plugin_data,
                    'badge_data'  => self::encodeBadge($all_badges[$slug]),
                ];
            }
        }

        PscCertifiedPluginsCacheRepository::replaceAll(array_merge($installed_rows, $catalog_rows));

        global $spbc;

        $spbc->data['psc_catalog_total'] = $catalog_total ?: count($catalog_rows);
        $spbc->data['psc_catalog_exhausted'] = true;
        $spbc->data['psc_cache_updated_at'] = time();
        $spbc->save('data');
    }

    /**
     * Read rows from the cache table starting at $db_offset, enrich and filter
     * by PSC version until we collect $per_page valid items or exhaust the table.
     *
     * @param int $db_offset Row offset in the cache table.
     * @param int $per_page
     * @return array{plugins: array<int, array<string, mixed>>, badges: array<string, array<string, mixed>>, db_offset: int}
     */
    public static function getPageWithEnrichment($db_offset, $per_page)
    {
        $db_offset = max(0, (int) $db_offset);
        $batch_size = $per_page * 2;

        require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

        $plugins = [];
        $badges = [];
        $total_rows = PscCertifiedPluginsCacheRepository::countRows();

        while (count($plugins) < $per_page && $db_offset < $total_rows) {
            $rows = PscCertifiedPluginsCacheRepository::getRows(null, $db_offset, $batch_size);

            if (empty($rows)) {
                break;
            }

            // Advance offset per processed row so an early break doesn't skip unvisited rows.
            foreach ($rows as $row) {
                $db_offset++;

                $slug = sanitize_title((string) ($row['slug'] ?? ''));
                $badge = self::decodeBadge((string) ($row['badge_data'] ?? ''));

                if (empty($slug) || empty($badge)) {
                    continue;
                }

                $plugin = self::decodePlugin((string) ($row['plugin_data'] ?? ''));
                $is_stub = $plugin && empty($plugin['num_ratings']) && empty($plugin['active_installs']);
                $is_stale = empty($plugin) || $is_stub || (time() - (int) ($row['updated_at'] ?? 0) > self::CACHE_TTL);

                if ($is_stale) {
                    $plugin = self::fetchAndCachePlugin($slug, (string) ($row['source'] ?? 'catalog'), $badge);
                }

                if (empty($plugin)) {
                    continue;
                }

                $report = $badge['report_object'] ?? null;
                if ($report instanceof PluginReport && ! self::isVersionInPscRange($report, (string) ($plugin['version'] ?? ''))) {
                    continue;
                }

                $plugins[] = $plugin;
                $badges[$slug] = $badge;

                if (count($plugins) >= $per_page) {
                    break;
                }
            }
        }

        return [
            'plugins'   => $plugins,
            'badges'    => $badges,
            'db_offset' => $db_offset,
        ];
    }

    /**
     * @param string $slug
     * @param string $source
     * @param array<string, mixed> $badge
     * @return array<string, mixed>|null
     */
    private static function fetchAndCachePlugin($slug, $source, $badge)
    {
        $catalog_item = [
            'slug'               => $slug,
            'app_name'           => $badge['app_name'] ?? $slug,
            'app_status'         => 'safe_psc',
            'psc'                => $badge['psc'] ?? '',
            'id'                 => $badge['id'] ?? '',
            'rs_app_version_max' => $badge['current_version'] ?? '',
        ];

        $plugin = PscCertifiedPluginsCatalogScanner::fetchPluginFromCatalogItem($catalog_item);

        if (! $plugin) {
            return null;
        }

        // Only cache real WP.org data, not fallback stubs with synthetic version.
        if (! empty($plugin['num_ratings']) || ! empty($plugin['active_installs'])) {
            PscCertifiedPluginsCacheRepository::updatePluginData($slug, $source, self::encodePlugin($plugin));
        }

        return $plugin;
    }

    /**
     * Search cached PSC plugins by name (partial match).
     *
     * @param string $term
     * @return array{plugins: array<int, array<string, mixed>>, badges: array<string, array<string, mixed>>}
     */
    public static function searchByName($term)
    {
        $rows = PscCertifiedPluginsCacheRepository::getRows(null, 0, 0);

        require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

        $term_lower = mb_strtolower(trim($term));
        $plugins = [];
        $badges = [];

        foreach ($rows as $row) {
            $slug = sanitize_title((string) ($row['slug'] ?? ''));
            $badge = self::decodeBadge((string) ($row['badge_data'] ?? ''));

            if (empty($slug) || empty($badge)) {
                continue;
            }

            $app_name = mb_strtolower((string) ($badge['app_name'] ?? $slug));
            $slug_as_name = mb_strtolower(str_replace('-', ' ', $slug));

            if (mb_strpos($app_name, $term_lower) === false && mb_strpos($slug_as_name, $term_lower) === false) {
                continue;
            }

            $plugin = self::decodePlugin((string) ($row['plugin_data'] ?? ''));

            if (empty($plugin)) {
                $plugin = self::fetchAndCachePlugin($slug, (string) ($row['source'] ?? 'catalog'), $badge);
            }

            if (empty($plugin)) {
                continue;
            }

            $report = $badge['report_object'] ?? null;
            if ($report instanceof PluginReport && ! self::isVersionInPscRange($report, (string) ($plugin['version'] ?? ''))) {
                continue;
            }

            $plugins[] = $plugin;
            $badges[$slug] = $badge;
        }

        return [
            'plugins' => $plugins,
            'badges'  => $badges,
        ];
    }

    /**
     * @return array{plugins: array<int, array<string, mixed>>, badges: array<string, array<string, mixed>>}
     */
    public static function getInstalledPlugins()
    {
        $version_map = self::getInstalledPluginsVersionMap();

        if (empty($version_map)) {
            return ['plugins' => [], 'badges' => []];
        }

        $rows = PscCertifiedPluginsCacheRepository::getRowsBySlugs(array_keys($version_map));

        require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

        $plugins = [];
        $badges = [];
        $seen = [];

        foreach ($rows as $row) {
            $slug = sanitize_title((string) ($row['slug'] ?? ''));
            $badge = self::decodeBadge((string) ($row['badge_data'] ?? ''));

            if (empty($slug) || empty($badge) || isset($seen[$slug])) {
                continue;
            }

            $report = $badge['report_object'] ?? null;
            $installed_version = $version_map[$slug] ?? '';

            if ($report instanceof PluginReport && $installed_version !== '' && ! self::isVersionInPscRange($report, $installed_version)) {
                continue;
            }

            $seen[$slug] = true;

            $plugin = self::decodePlugin((string) ($row['plugin_data'] ?? ''));

            if (empty($plugin)) {
                $plugin = self::fetchAndCachePlugin($slug, (string) ($row['source'] ?? 'catalog'), $badge);
            }

            if (empty($plugin)) {
                continue;
            }

            $plugins[] = $plugin;
            $badges[$slug] = $badge;
        }

        return ['plugins' => $plugins, 'badges' => $badges];
    }

    /**
     * Slug to PSC certificate ID map of the cached certified plugins.
     *
     * @param array<int, string> $slugs Restrict the map to these slugs, all the cached rows if empty.
     *
     * @return array<string, string>
     */
    public static function getCertificatesMap($slugs = array())
    {
        $rows = PscCertifiedPluginsCacheRepository::getRowsBySlugs($slugs);

        $certificates = [];

        foreach ($rows as $row) {
            $slug = sanitize_title((string) ($row['slug'] ?? ''));
            $badge = self::decodeBadge((string) ($row['badge_data'] ?? ''));

            $certificate = (string) ($badge['psc'] ?? '');

            if (empty($slug) || empty($certificate) || isset($certificates[$slug])) {
                continue;
            }

            $certificates[$slug] = $certificate;
        }
        return $certificates;
    }

    /**
     * @return int
     */
    public static function countAll()
    {
        return PscCertifiedPluginsCacheRepository::countRows();
    }

    /**
     * @return int
     */
    public static function countInstalled()
    {
        $version_map = self::getInstalledPluginsVersionMap();

        if (empty($version_map)) {
            return 0;
        }

        $rows = PscCertifiedPluginsCacheRepository::getRowsBySlugs(array_keys($version_map));
        $seen = [];
        $count = 0;

        foreach ($rows as $row) {
            $slug = sanitize_title((string) ($row['slug'] ?? ''));
            $badge = self::decodeBadge((string) ($row['badge_data'] ?? ''));

            if (empty($slug) || empty($badge) || isset($seen[$slug])) {
                continue;
            }

            $report = $badge['report_object'] ?? null;
            $installed_version = $version_map[$slug] ?? '';

            if ($report instanceof PluginReport && $installed_version !== '' && ! self::isVersionInPscRange($report, $installed_version)) {
                continue;
            }

            $seen[$slug] = true;
            $count++;
        }

        return $count;
    }

    /**
     * @return int
     */
    public static function countCertifiedLastMonth()
    {
        $rows = PscCertifiedPluginsCacheRepository::getRows(null, 0, 0);
        $one_month_ago = strtotime('-1 month');
        $count = 0;

        foreach ($rows as $row) {
            $badge = self::decodeBadge((string) ($row['badge_data'] ?? ''));

            if (empty($badge)) {
                continue;
            }

            $pub_date = (string) ($badge['publication_date'] ?? '');

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

            $timestamp = strtotime($pub_date);

            if ($timestamp !== false && $timestamp >= $one_month_ago) {
                $count++;
            }
        }

        return $count;
    }

    /**
     * Returns true when the given version falls within the PSC-certified range.
     *
     * @param PluginReport $report
     * @param string $version
     * @return bool
     */
    private static function isVersionInPscRange($report, $version)
    {
        if ($version === '') {
            return true;
        }

        if (! empty($report->rs_app_version_max) && version_compare($version, $report->rs_app_version_max, '>')) {
            return false;
        }

        if (! empty($report->rs_app_version_min) && version_compare($version, $report->rs_app_version_min, '<')) {
            return false;
        }

        return true;
    }

    /**
     * @return array<string, string> slug => installed version
     */
    private static function getInstalledPluginsVersionMap()
    {
        if (! function_exists('get_plugins')) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        $map = [];

        foreach (get_plugins() as $plugin_file => $plugin_data) {
            $slug = sanitize_title(dirname((string) $plugin_file));

            // Single-file plugins (e.g. hello.php) have no directory; fall back to TextDomain.
            if ($slug === '' || $slug === '.') {
                $slug = ! empty($plugin_data['TextDomain'])
                    ? sanitize_title((string) $plugin_data['TextDomain'])
                    : '';
            }

            if ($slug !== '' && $slug !== '.' && ! isset($map[$slug])) {
                $map[$slug] = (string) ($plugin_data['Version'] ?? '');
            }
        }

        return $map;
    }

    /**
     * @return array<int, array{slug: string, source: string, sort_order: int, plugin_data: string, badge_data: string}>
     */
    private static function buildInstalledRows()
    {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';
        require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

        self::ensureVulnerabilityLibrary();

        $safe_plugins = VulnerabilityAlarm::getSafePlugins(get_plugins(), false);
        $rows = [];
        $sort_order = 0;

        foreach ($safe_plugins as $safe_plugin) {
            $slug = sanitize_title((string) ($safe_plugin['slug'] ?? ''));

            if (empty($slug) || empty($safe_plugin['psc'])) {
                continue;
            }

            $report = $safe_plugin['report_object'] ?? null;
            $plugin = PscCertifiedPluginsCatalogScanner::fetchPluginFromCatalogItem([
                'slug'               => $slug,
                'app_name'           => $report instanceof PluginReport && ! empty($report->app_name) ? $report->app_name : $slug,
                'rs_app_version_max' => $safe_plugin['current_version'] ?? '',
                'psc'                => $safe_plugin['psc'],
                'id'                 => $safe_plugin['id'] ?? '',
                'app_status'         => 'safe_psc',
            ]);

            if (! $plugin) {
                continue;
            }

            $rows[] = [
                'slug'        => $slug,
                'source'      => PscCertifiedPluginsCacheRepository::SOURCE_INSTALLED,
                'sort_order'  => $sort_order++,
                'plugin_data' => self::encodePlugin($plugin),
                'badge_data'  => self::encodeBadge($safe_plugin),
            ];
        }

        return $rows;
    }

    /**
     * Installed plugins are matched against the locally stored vulnerability reports,
     * so the library has to exist before the very first warm-up.
     *
     * @return void
     */
    private static function ensureVulnerabilityLibrary()
    {
        if (get_option(VulnerabilityAlarm::VULNERABILITY_LIST) instanceof ApiResults) {
            return;
        }

        try {
            VulnerabilityAlarm::updateWPModulesVulnerabilities();
        } catch (\Exception $exception) {
            spbc_log('PSC cache: unable to update vulnerabilities library. ' . $exception->getMessage());
        }
    }

    /**
     * @param array<string, mixed>|object $plugin
     * @return string
     */
    private static function encodePlugin($plugin)
    {
        return (string) wp_json_encode(PscCertifiedPluginsCatalogScanner::toArray($plugin));
    }

    /**
     * @param string $json
     * @return array<string, mixed>|null
     */
    private static function decodePlugin($json)
    {
        if ($json === '') {
            return null;
        }

        $decoded = json_decode($json, true);

        return is_array($decoded) ? $decoded : null;
    }

    /**
     * @param array<string, mixed> $badge
     * @return string
     */
    private static function encodeBadge($badge)
    {
        $payload = $badge;

        if (($payload['report_object'] ?? null) instanceof PluginReport) {
            $payload['report_object'] = get_object_vars($payload['report_object']);
        }

        return (string) wp_json_encode($payload);
    }

    /**
     * @param string $json
     * @return array<string, mixed>|null
     */
    private static function decodeBadge($json)
    {
        if ($json === '') {
            return null;
        }

        $decoded = json_decode($json, true);

        if (! is_array($decoded)) {
            return null;
        }

        if (is_array($decoded['report_object'] ?? null)) {
            $report = new PluginReport();

            foreach ($decoded['report_object'] as $key => $value) {
                if (property_exists($report, $key) && (is_scalar($value) || $value === null)) {
                    $report->$key = (string) $value;
                }
            }

            $decoded['report_object'] = $report;
        }

        return $decoded;
    }

    /**
     * @return void
     */
    private static function ensureTableExists()
    {
        $tables_analyzer = new \CleantalkSP\SpbctWP\DB\TablesAnalyzer();

        foreach ($tables_analyzer->getNotExistingTables() as $not_existing_table) {
            if ($not_existing_table !== SPBC_TBL_PSC_PLUGINS_CACHE) {
                continue;
            }

            $db_tables_creator = new \CleantalkSP\SpbctWP\DB\TablesCreator();
            $db_tables_creator->createTable($not_existing_table);
        }
    }
}
