<?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Vulnerability;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\PluginController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\ThemeController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityDataController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityNoticeController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Controllers\VulnerabilityNotificationController;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\VulnerabilitySyncService;
use ReallySimplePlugins\RSS\Core\Interfaces\FeatureInterface;
use ReallySimplePlugins\RSS\Core\Support\Helpers\Storages\EnvironmentConfig;
use ReallySimplePlugins\RSS\Core\Traits\HasScheduler;
use ReallySimplePlugins\RSS\Core\Traits\HasViews;

/**
 * Runs vulnerability sync hooks.
 *
 * Daily cron does full sync.
 * Update and activation hooks queue small syncs.
 */
final class VulnerabilityController implements FeatureInterface
{
    use HasViews;
    use HasScheduler;

    /**
     * Stores queued sync work until cron runs.
     */
    private const PENDING_SYNC_OPTION = 'rsssl_vulnerability_pending_sync';

    /**
     * Cron hook that runs the queued sync.
     */
    private const SYNC_EVENT = 'rsssl_vulnerability_run_scheduled_sync';

    /**
     * Fired after sync is done.
     */
    public const SYNC_COMPLETED_ACTION = 'rsssl_vulnerability_sync_completed';

    /**
     * Wait time before queued sync runs.
     */
    private const SYNC_DEBOUNCE = (5 * MINUTE_IN_SECONDS);

    private VulnerabilitySyncService $vulnerabilitySyncService;
    private EnvironmentConfig $env;

    public function __construct(
        VulnerabilitySyncService $vulnerabilitySyncService,
        EnvironmentConfig $environmentConfig
    ) {
        $this->vulnerabilitySyncService = $vulnerabilitySyncService;
        $this->env = $environmentConfig;
    }

    /**
     * Wire all hooks for this feature.
     */
    public function register(): void
    {
        add_action('admin_enqueue_scripts', [$this, 'enqueueStyles']);
        add_action(self::SYNC_EVENT, [$this, 'runSync'], 10, 0);
        add_filter('rss_core_controller_classes', [$this, 'registerControllers']);
        add_action('rss_core_activation', [$this, 'scheduleFullSync'], 10, 1);
        add_action('upgrader_process_complete', [$this, 'scheduleUpgradeSync'], 10, 2);
        add_action('activate_plugin', [$this, 'schedulePluginActivationSync'], 10, 2);
        add_action('after_switch_theme', [$this, 'scheduleThemeSwitchSync'], 10, 0);
        add_action('_core_updated_successfully', [$this, 'scheduleCoreUpdateSync'], 10, 1);
        add_action('rsssl_daily_cron', [$this, 'scheduleFullSync']);
    }

    /**
     * Registers the controllers related to vulnerability management. This
     * method is hooked into the 'rss_core_controller_classes' filter, that
     * is applied here {@see Plugin::registerControllers}, to make sure the
     * {@see ControllerManager} can register them in the plugin lifecycle.
     */
    public function registerControllers(array $existingControllers): array
    {
        $enablePluginAndThemeDisplay = rsssl_get_option('enable_feedback_in_plugin', false);
        $availableControllers = [
            VulnerabilityDataController::class,
            VulnerabilityNoticeController::class,
            VulnerabilityNotificationController::class,
        ];
        if ($enablePluginAndThemeDisplay) {
            $availableControllers[] = PluginController::class;
            $availableControllers[] = ThemeController::class;
        }
        return array_merge($existingControllers, $availableControllers);
    }

    /**
     * Enqueues shared vulnerability styling on plugins and themes overview pages.
     * we register this here because both the PluginController and ThemeController
     * needs the same styles.
     *
     * @param null|string $hook Current admin page hook suffix. Can be null in
     * non-default admin pages. For example when using a page builder plugin.
    */
    public function enqueueStyles(?string $hook = null): void
    {
        if (empty($hook) || !$this->isComponentOverviewScreen($hook)) {
            return;
        }

        $rtl = is_rtl() ? 'rtl/' : '';
        $assetsUrl  = trailingslashit($this->env->getString('plugin.assets_url'));
        $assetsPath = trailingslashit($this->env->getString('plugin.assets_path'));

        $url  = $assetsUrl . "css/{$rtl}rsssl-plugin.min.css";
        $path = $assetsPath . "css/{$rtl}rsssl-plugin.min.css";
        $version = $this->env->get('plugin.version');
        if (file_exists($path)) {
            wp_enqueue_style('rsssl-plugin', $url, [], $version);
        }
    }

    /**
     * Determine whether the current admin page is a plugin or theme overview screen.
     *
     * Supports both regular admin and multisite network-admin pages.
     */
    private function isComponentOverviewScreen(string $hook): bool
    {
        return in_array($hook, ['plugins.php', 'plugins-network.php', 'themes.php', 'themes-network.php'], true);
    }

    /**
     * Queue full sync. Full beats scoped.
     *
     * Daily cron and product activation use this.
     */
    public function scheduleFullSync(...$args): void
    {
        $pendingSync = $this->getPendingSync();
        $pendingSync['full'] = true;
        $pendingSync['components'] = [];

        $this->storePendingSync($pendingSync);
        $this->scheduleDebounced(self::SYNC_EVENT, self::SYNC_DEBOUNCE, []);
    }

    /**
     * Queue one plugin sync from activation.
     *
     * Network-wide activation uses the same scoped sync.
     * Vulnerability data is per component, not per site.
     */
    public function schedulePluginActivationSync(string $pluginFile = '', bool $networkWide = false): void
    {
        $slug = $this->getPluginSlugFromPath($pluginFile);
        if ($slug === '') {
            return;
        }

        $this->queueScopedSync('plugin', $slug);
    }

    /**
     * Queue scoped syncs from upgrader events.
     *
     * If the upgrader payload is unclear, skip the event.
     */
    public function scheduleUpgradeSync($upgrader = null, array $hookExtra = []): void
    {
        $action = isset($hookExtra['action']) && is_string($hookExtra['action']) ? $hookExtra['action'] : '';
        if ($action !== 'update') {
            return;
        }

        $type = isset($hookExtra['type']) && is_string($hookExtra['type']) ? $hookExtra['type'] : '';
        if ($type === 'plugin') {
            $this->schedulePluginUpgradeSync($hookExtra['plugins'] ?? []);
            return;
        }

        if ($type === 'theme') {
            $this->scheduleThemeUpgradeSync($hookExtra['themes'] ?? []);
            return;
        }

        if ($type === 'core') {
            $this->scheduleCoreUpgradeSync();
            return;
        }
    }

    /**
     * Queue scoped plugin syncs from an upgrader payload.
     *
     * @param mixed $plugins Upgrader payload plugin files.
     */
    private function schedulePluginUpgradeSync($plugins): void
    {
        foreach ($this->normalizeComponentSlugsFromUpgradePayload($plugins, 'plugin') as $slug) {
            $this->queueScopedSync('plugin', $slug);
        }
    }

    /**
     * Queue scoped theme syncs from an upgrader payload.
     *
     * @param mixed $themes Upgrader payload theme slugs.
     */
    private function scheduleThemeUpgradeSync($themes): void
    {
        foreach ($this->normalizeComponentSlugsFromUpgradePayload($themes, 'theme') as $slug) {
            $this->queueScopedSync('theme', $slug);
        }
    }

    /**
     * Queue scoped core sync from an upgrader payload.
     */
    private function scheduleCoreUpgradeSync(): void
    {
        $this->queueScopedSync('core', 'wordpress');
    }

    /**
     * Queue one theme sync after theme switch.
     */
    public function scheduleThemeSwitchSync(): void
    {
        $theme = wp_get_theme();
        $slug = sanitize_key((string) $theme->get_stylesheet());

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

        $this->queueScopedSync('theme', $slug);
    }

    /**
     * Queue core sync after core update.
     */
    public function scheduleCoreUpdateSync(...$args): void
    {
        $this->queueScopedSync('core', 'wordpress');
    }

    /**
     * Run queued sync.
     *
     * Full sync only runs when full=true.
     * Components sync only runs when components exist.
     * Empty queue stops here, no full sync fallback.
     */
    public function runSync(): void
    {
        $pendingSync = $this->getPendingSync();

        try {
            if ($pendingSync['full'] ?? false) {
                $this->vulnerabilitySyncService->syncAllComponents();
            } elseif (!empty($pendingSync['components'])) {
                $this->vulnerabilitySyncService->syncSelectedComponents(array_values($pendingSync['components']));
            } else {
                delete_option(self::PENDING_SYNC_OPTION);
                return;
            }

            delete_option(self::PENDING_SYNC_OPTION);
        } finally {
            // Release the debounce lock so future triggers can schedule again.
            $this->releaseDebounceLock(self::SYNC_EVENT);
        }

        // Notify that the sync has completed.
        do_action(self::SYNC_COMPLETED_ACTION);
    }

    /**
     * Add one component to the queued scoped sync.
     *
     * Full sync stays stronger than any queued scoped work.
     */
    private function queueScopedSync(string $type, string $slug): void
    {
        $type = strtolower(trim($type));
        $slug = strtolower(trim($slug));

        if ($type === '' || $slug === '') {
            return;
        }

        $pendingSync = $this->getPendingSync();
        if (!($pendingSync['full'] ?? false)) {
            if (!isset($pendingSync['components']) || !is_array($pendingSync['components'])) {
                $pendingSync['components'] = [];
            }

            $componentKey = $type . ':' . $slug;
            $pendingSync['components'][$componentKey] = [
                'type' => $type,
                'slug' => $slug,
            ];
        }

        $this->storePendingSync($pendingSync);
        $this->scheduleDebounced(self::SYNC_EVENT, self::SYNC_DEBOUNCE, []);
    }

    /**
     * Read queued sync state from the option.
     *
     * Bad rows are ignored so one broken value does not break the queue.
     */
    private function getPendingSync(): array
    {
        $pendingSync = get_option(self::PENDING_SYNC_OPTION, []);
        if (!is_array($pendingSync)) {
            return [
                'full' => false,
                'components' => [],
            ];
        }

        if (!isset($pendingSync['components']) || !is_array($pendingSync['components'])) {
            return [
                'full' => !empty($pendingSync['full']),
                'components' => [],
            ];
        }

        $components = [];
        foreach ($pendingSync['components'] as $componentKey => $component) {
            if (!is_array($component)) {
                continue;
            }

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

            $components[(string) $componentKey] = [
                'type' => $type,
                'slug' => $slug,
            ];
        }

        return [
            'full' => !empty($pendingSync['full']),
            'components' => $components,
        ];
    }

    /**
     * Save queued sync state.
     */
    private function storePendingSync(array $pendingSync): void
    {
        update_option(self::PENDING_SYNC_OPTION, $pendingSync, false);
    }

    /**
     * Turn plugin path into plugin slug.
     */
    private function getPluginSlugFromPath(string $pluginPath): string
    {
        $pluginPath = trim(plugin_basename($pluginPath));
        if ($pluginPath === '') {
            return '';
        }

        $directory = dirname($pluginPath);
        $slug = $directory !== '.' && $directory !== '/' ? $directory : basename($pluginPath, '.php');

        return strtolower($slug);
    }

    /**
     * Extract slugs from upgrader payload.
     *
     * Plugins use plugin-file to slug conversion.
     * Themes use the stylesheet slug as-is.
     *
     * @param mixed $items Upgrader payload items.
     */
    private function normalizeComponentSlugsFromUpgradePayload($items, string $type): array
    {
        if (!is_array($items)) {
            return [];
        }

        $slugs = [];
        foreach ($items as $item) {
            if (!is_string($item) || $item === '') {
                continue;
            }

            $slug = sanitize_key($item);

            if ($type === 'plugin') {
                $slug = $this->getPluginSlugFromPath($item);
            }

            if ($slug !== '' && !in_array($slug, $slugs, true)) {
                $slugs[] = $slug;
            }
        }

        return $slugs;
    }

}
