<?php

declare(strict_types=1);

namespace ReallySimplePlugins\RSS\Core\Features\Pro\Vulnerability\Services;

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\PluginRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\ThemeRepository;

/**
 * Performs silent plugin/theme updates using WordPress upgrader APIs.
 *
 * What this does:
 * - Measures trigger an "attempt" to update a vulnerable component.
 * - WordPress upgraders can report a successful execution even when no update
 *   was applied (e.g. no update available, or an internal no-op).
 * - For measures, we must be strict: only treat an attempt as successful when
 *   WordPress offered an update AND the installed version actually changed.
 */
final class VulnerabilityAutoUpdateService
{
    private const TYPE_PLUGIN = 'plugin';
    private const TYPE_THEME = 'theme';
    private const SKIP_NEXT_MEASURES_SYNC_OPTION = 'rsssl_pro_skip_next_vulnerability_measures_sync';

    private PluginRepository $pluginRepository;
    private ThemeRepository $themeRepository;

    public function __construct(
        PluginRepository $pluginRepository,
        ThemeRepository $themeRepository
    )
    {
        $this->pluginRepository = $pluginRepository;
        $this->themeRepository = $themeRepository;
    }

    /**
     * Attempt to update a single component.
     *
     * The controller expects a boolean that answers one question:
     * "Did we actually apply an update?"
     *
     * Notes:
     * - We only support components of type `plugin` or `theme`.
     * - Success is strict: an update must be offered by WordPress (transient)
     *   and the installed version must change after running the upgrader.
     * - Any other outcome (no update available, failure, or no version change)
     *   returns false so measures can count attempts and apply next steps.
     *
     * @param array{
     *   type: 'plugin'|'theme',
     *   plugin_file?: string,
     *   component_slug: string
     * } $measure
     *
     * @return bool True only when an update was actually applied.
     */
    public function update(array $measure): bool
    {
        $type = (string) ($measure['type'] ?? '');

        if ($type === self::TYPE_THEME) {
            return $this->updateTheme((string) ($measure['component_slug'] ?? ''));
        }

        if ($type === self::TYPE_PLUGIN) {
            return $this->updatePlugin((string) ($measure['plugin_file'] ?? ''));
        }

        return false;
    }

    /**
     * Check whether the next measures run should be skipped.
     *
     * The flag is set after a successful auto-update. Consuming it here keeps
     * the post-update vulnerability refresh, but lets the caller skip one
     * follow-up measures run so the update flow does not immediately restart.
     */
    public function shouldSkipMeasures(): bool
    {
        $shouldSkip = (bool) get_option(self::SKIP_NEXT_MEASURES_SYNC_OPTION, false);
        if (!$shouldSkip) {
            return false;
        }

        delete_option(self::SKIP_NEXT_MEASURES_SYNC_OPTION);

        return true;
    }

    /**
     * Perform a plugin update attempt and report whether an update was applied.
     *
     * Why the extra checks?
     * WordPress' upgrader APIs can return a "successful" result array even when
     * nothing was updated. For vulnerability measures we need to distinguish:
     * - "upgrade executed" vs
     * - "plugin actually updated"
     *
     * Flow:
     * 1) Refresh plugin update data (`wp_update_plugins()`).
     * 2) Abort early if WordPress offers no update for this plugin.
     * 3) Capture the currently installed version.
     * 4) Run the bulk upgrader for this single plugin.
     * 5) Clear caches and re-check installed version.
     * 6) Return true only if the version changed.
     *
     * @param string $pluginFile Plugin basename (e.g. my-plugin/my-plugin.php).
     *
     * @return bool True only when an update was offered and the version changed.
     */
    private function updatePlugin(string $pluginFile): bool
    {
        if ($pluginFile === '') {
            return false;
        }

        // Refresh plugin update data so we can reliably detect whether an update is actually offered.
        wp_update_plugins();

        if (! $this->pluginRepository->hasPluginUpdateAvailable($pluginFile)) {
            return false;
        }

        $beforeVersion = $this->pluginRepository->getPluginVersion($pluginFile);

        $this->includeUpgraderFiles(self::TYPE_PLUGIN);

        $upgrader = new \Plugin_Upgrader($this->createSkin('upgrade-plugin_' . $pluginFile));

        if (! $this->bulkUpgrade($upgrader, [$pluginFile])) {
            return false;
        }

        // Clear caches and re-read the version.
        wp_clean_plugins_cache(true);
        wp_update_plugins();

        $afterVersion = $this->pluginRepository->getPluginVersion($pluginFile);
        $updated = $this->validateSuccessfulUpdate($beforeVersion, $afterVersion);
        if ($updated) {
            $this->markNextMeasuresSyncSkip();
        }

        return $updated;
    }

    /**
     * Perform a theme update attempt and report whether an update was applied.
     *
     * Mirrors the plugin flow:
     * 1) Refresh theme update data (`wp_update_themes()`).
     * 2) Abort early if WordPress offers no update for this theme.
     * 3) Capture the currently installed version.
     * 4) Run the bulk upgrader for this single theme.
     * 5) Clear caches and re-check installed version.
     * 6) Return true only if the version changed.
     *
     * @param string $stylesheet Theme stylesheet slug (directory name).
     *
     * @return bool True only when an update was offered and the version changed.
     */
    private function updateTheme(string $stylesheet): bool
    {
        if ($stylesheet === '') {
            return false;
        }

        // Refresh theme update data so we can reliably detect whether an update is actually offered.
        wp_update_themes();

        if (! $this->themeRepository->hasThemeUpdateAvailable($stylesheet)) {
            return false;
        }

        $beforeVersion = $this->themeRepository->getThemeVersion($stylesheet);

        $this->includeUpgraderFiles(self::TYPE_THEME);

        $upgrader = new \Theme_Upgrader($this->createSkin('upgrade-theme_' . $stylesheet));

        if (! $this->bulkUpgrade($upgrader, [$stylesheet])) {
            return false;
        }

        // Re-read the version.
        wp_clean_themes_cache(true);
        wp_update_themes();

        $afterVersion = $this->themeRepository->getThemeVersion($stylesheet);
        $updated = $this->validateSuccessfulUpdate($beforeVersion, $afterVersion);
        if ($updated) {
            $this->markNextMeasuresSyncSkip();
        }

        return $updated;
    }

    /**
     * Check whether the installed component version changed.
     */
    private function validateSuccessfulUpdate(string $beforeVersion, string $afterVersion): bool
    {
        return ($beforeVersion !== $afterVersion);
    }

    /**
     * Mark the next measures scheduling check as already handled.
     *
     * A successful auto-update triggers a vulnerability refresh. The refresh
     * should still run, but the measures scheduler should consume this flag and
     * skip one follow-up measures run.
     */
    private function markNextMeasuresSyncSkip(): void
    {
        update_option(self::SKIP_NEXT_MEASURES_SYNC_OPTION, true, false);
    }

    /**
     * Load the WordPress upgrader dependencies.
     *
     * WordPress keeps these files under wp-admin, so we include them only when
     * needed to keep this service easy to reason about.
     *
     * @param string $type One of self::TYPE_PLUGIN or self::TYPE_THEME.
     */
    private function includeUpgraderFiles(string $type): void
    {
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

        if ($type === self::TYPE_PLUGIN) {
            require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
            return;
        }

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

    /**
     * Create the upgrader skin used for silent updates.
     *
     * @param string $nonceAction The action used to generate the WP nonce.
     */
    private function createSkin(string $nonceAction): \Automatic_Upgrader_Skin
    {
        return new \Automatic_Upgrader_Skin([
            'title' => __('Upgrading...', 'really-simple-ssl'),
            'type' => 'auto',
            'nonce' => wp_create_nonce($nonceAction),
        ]);
    }

    /**
     * Run a bulk upgrade and normalize the meaning of "success".
     *
     * WordPress bulk upgraders typically return an array keyed by item where the
     * value is `true` on success and `WP_Error`/falsey on failure. Importantly,
     * the returned array can be non-empty even when some items failed.
     *
     * For measures we interpret "success" as:
     * - every requested item exists in the result array, AND
     * - every requested item is strictly `true`.
     *
     * Anything else counts as failure so the controller can increment attempts.
     *
     * @param object $upgrader \Plugin_Upgrader or \Theme_Upgrader.
     * @param list<string> $items Plugin files or theme stylesheets.
     *
     * @return bool True only when all items reported strict success.
     */
    private function bulkUpgrade(object $upgrader, array $items): bool
    {
        $result = $upgrader->bulk_upgrade($items, [
            'clear_update_cache' => true,
            'clear_update_transient' => true,
        ]);

        if (! is_array($result) || $result === []) {
            return false;
        }

        // `bulk_upgrade()` returns an array keyed by item, where each value is:
        // - true on success
        // - WP_Error / false / null on failure
        // We only consider the upgrade successful if *all* requested items succeeded.
        foreach ($items as $item) {
            if (! array_key_exists($item, $result)) {
                return false;
            }

            $itemResult = $result[$item];

            if ($itemResult === true) {
                continue;
            }

            if (is_wp_error($itemResult)) {
                return false;
            }

            return false;
        }

        return true;
    }
}
