<?php

declare(strict_types=1);

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

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Policies\ConfigurableSeverityPolicy;

/**
 * Reads and writes the vulnerability measures configuration.
 *
 * This service is used by the admin settings UI:
 * - `getMeasures()` provides a list of measures (id/name/value/description) the UI can render.
 * - `saveMeasures()` persists submitted values back to WordPress options.
 *
 * How to add a new measure:
 * 1) Add a new OPTION_* constant for the option key.
 * 2) Extend `getMeasures()` to return a new item with a unique `id`.
 * 3) Extend `saveMeasures()` to store the submitted value for that `id`.
 * 4) If the new measure depends on other measures, add/extend a small helper like
 *    `applyCascadingQuarantine()` to enforce the invariant in one place.
 *
 * Constraints:
 * - Uses WordPress option storage only; no external I/O.
 */
final class VulnerabilityMeasuresConfigService
{
    public const OPTION_FORCE_UPDATE = 'rsssl_force_update';
    public const OPTION_QUARANTINE = 'rsssl_quarantine';

    /**
     * Build the measures list for the settings UI.
     *
     * The returned array is consumed by the admin screen. Each item describes a single measure:
     * - `id` is the stable identifier used by the UI and `saveMeasures()`.
     * - `value` is the currently stored option (normalized).
     * - `disabledSeverities` can be used by the UI to disable invalid selections.
     *
     * @return array<int, array{
     *   id: string,
     *   name: string,
     *   value: string,
     *   description: string,
     *   disabledSeverities?: array<int, string>
     * }>
     */
    public function getMeasures(): array
    {
        $forceUpdate = $this->normalizeSeverity((string) get_option(self::OPTION_FORCE_UPDATE, '*'));
        $quarantine = $this->normalizeSeverity((string) get_option(self::OPTION_QUARANTINE, '*'));

        $forceScore = $this->score($forceUpdate);
        $quarantine = $this->applyCascadingQuarantine($forceUpdate, $quarantine);

        $disabledForQuarantine = ['*'];
        foreach (ConfigurableSeverityPolicy::SEVERITY_SCORES as $severity => $score) {
            if ($score < $forceScore) {
                $disabledForQuarantine[] = $severity;
            }
        }

        return [
            [
                'id' => 'force_update',
                'name' => __('Force update', 'really-simple-ssl'),
                'value' => $forceUpdate,
                'description' => __('Automatically attempt to update vulnerable plugins and themes.', 'really-simple-ssl'),
            ],
            [
                'id' => 'quarantine',
                'name' => __('Quarantine', 'really-simple-ssl'),
                'value' => $quarantine,
                'description' => __('Quarantine vulnerable components if updates fail.', 'really-simple-ssl'),
                'disabledSeverities' => $disabledForQuarantine,
            ],
        ];
    }

    /**
     * Persist submitted measure values.
     *
     * The settings UI posts an array of items shaped:
     *   [ ['id' => 'force_update', 'value' => 'high'], ['id' => 'quarantine', 'value' => 'critical'] ]
     *
     *
     * @param array<int, array{id: string, value: mixed}> $riskData
     */
    public function saveMeasures(array $riskData): void
    {
        foreach ($riskData as $item) {
            $itemStorage = new Storage($item);

            $value = $this->normalizeSeverity(
                $itemStorage->getString('value', '*')
            );

            if ($itemStorage->getString('id') === 'force_update') {
                update_option(self::OPTION_FORCE_UPDATE, $value, false);
            }

            if ($itemStorage->getString('id') === 'quarantine') {
                update_option(self::OPTION_QUARANTINE, $value, false);
            }
        }

        // Enforce cascading in stored options too.
        $forceUpdate = $this->normalizeSeverity((string) get_option(self::OPTION_FORCE_UPDATE, '*'));
        $quarantine = $this->normalizeSeverity((string) get_option(self::OPTION_QUARANTINE, '*'));

        $quarantine = $this->applyCascadingQuarantine($forceUpdate, $quarantine);

        update_option(self::OPTION_QUARANTINE, $quarantine, false);
    }

    /**
     * Enforce cascading rules between force-update and quarantine.
     *
     * Quarantine must never be less strict than force-update.
     * If force-update is disabled ("*"), quarantine is forced to "*".
     */
    private function applyCascadingQuarantine(string $forceUpdate, string $quarantine): string
    {
        $forceScore = $this->score($forceUpdate);
        $quarantineScore = $this->score($quarantine);

        if ($forceScore === 0) {
            return '*';
        }

        if ($quarantineScore > 0 && $quarantineScore < $forceScore) {
            return $this->severityFromScore($forceScore) ?? 'critical';
        }

        return $quarantine;
    }

    /**
     * Normalize a severity value coming from user input or stored options.
     *
     * Returns "*" for "disabled / not set" and supports older one-letter values for backward compatibility.
     */
    private function normalizeSeverity(string $raw): string
    {
        $raw = strtolower(trim($raw));

        if ($raw === '' || $raw === '*') {
            return '*';
        }

        // Backward-compatibility for older stored values.
        if ($raw === 'l') {
            return 'low';
        }
        if ($raw === 'm') {
            return 'medium';
        }
        if ($raw === 'h') {
            return 'high';
        }
        if ($raw === 'c') {
            return 'critical';
        }

        return array_key_exists($raw, ConfigurableSeverityPolicy::SEVERITY_SCORES) ? $raw : '*';
    }

    /**
     * Convert a normalized severity to a numeric score for comparisons.
     */
    private function score(string $severity): int
    {
        if ($severity === '*') {
            return 0;
        }

        return ConfigurableSeverityPolicy::SEVERITY_SCORES[$severity] ?? 0;
    }

    /**
     * Convert a numeric score back to its severity label.
     */
    private function severityFromScore(int $score): ?string
    {
        foreach (ConfigurableSeverityPolicy::SEVERITY_SCORES as $severity => $value) {
            if ($value === $score) {
                return $severity;
            }
        }

        return null;
    }
}
