<?php

declare(strict_types=1);

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

use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\VulnerabilitySnapshotDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\VulnerabilityNotificationPolicyInterface;
use ReallySimplePlugins\RSS\Core\Services\EmailService;

/**
 * Policy that decides whether a vulnerability notification email should be sent
 * based on a configurable severity threshold.
 *
 * This class represents a *Policy* in the Policy/Strategy sense: it encapsulates
 * a single, well-defined decision rule that can be evaluated independently of
 * when or how notifications are sent.
 *
 * The decision is based on:
 * - The highest severity present in the current vulnerability snapshot
 * - A user-configured severity threshold
 * - Changes compared to the previous snapshot (to avoid redundant notifications)
 *
 * Responsibilities:
 * - Evaluate whether the current state meets or exceeds the configured threshold
 * - Detect meaningful changes between snapshots that justify a new notification
 * - Provide a human-readable reason explaining the decision
 *
 * This policy is intended to be composed by a higher-level notification or
 * scheduling mechanism, not to perform any dispatching itself.
 */
final class ConfigurableSeverityPolicy implements VulnerabilityNotificationPolicyInterface
{
    private EmailService $emailService;
    /**
     * Mapping of severity levels to numeric scores for comparison.
     * @var array<string, int>
     */
    public const SEVERITY_SCORES = [
        'low' => 1,
        'medium' => 2,
        'high' => 3,
        'critical' => 4,
    ];

    /**
     * @var string Reason for the last decision made by shouldSend().
     */
    private string $reason = 'No decision yet';

    /**
     * Reason prefix for searching in error logs.
     * @var string Prefix for the reason message.
     */
    private string $reasonPrefix = 'RSS policy: ';

    public function __construct(EmailService $emailService)
    {
        $this->emailService = $emailService;
    }

    /**
     * Determine if a notification should be sent based on the current and previous snapshots.
     *
     * @param VulnerabilitySnapshotDto $current
     * @param VulnerabilitySnapshotDto|null $previous
     * @return bool Whether a notification should be sent based on the current and previous snapshots.
     */
    public function shouldSend(VulnerabilitySnapshotDto $current, ?VulnerabilitySnapshotDto $previous): bool
    {
        $threshold = '';
        $userWantsNotificationEmail = $this->emailService->isNotificationsEnabled();
        if (!$userWantsNotificationEmail) {
            $this->reason = 'User setting disables emails.';
            return false;
        }

        if (function_exists('rsssl_get_option')) {
            $threshold = (string) rsssl_get_option('vulnerability_notification_email_admin');
        }

        $threshold = strtolower($threshold);

        if ($threshold === '*' || $threshold === '') {
            $this->reason = 'User setting disables emails.';
            return false;
        }

        if ($current->getHighestSeverity() === null) {
            $this->reason = 'No vulnerabilities present.';
            return false;
        }

        $thresholdScore = self::SEVERITY_SCORES[$threshold] ?? 0;
        $currentScore = self::SEVERITY_SCORES[strtolower($current->getHighestSeverity())] ?? 0;

        if ($currentScore < $thresholdScore) {
            $this->reason = 'Highest severity below user threshold.';
            return false;
        }

        if ($previous === null) {
            $this->reason = 'First snapshot and highest severity meets/exceeds threshold.';
            return true;
        }

        $previousGeneratedAt = $previous->getGeneratedAt();
        $currentGeneratedAt  = $current->getGeneratedAt();
        $previousSnapshotOlderThanOneDay = (($currentGeneratedAt - $previousGeneratedAt) >= DAY_IN_SECONDS);

        // Vulnerabilities still exist and no changes were made for one day, send notification
        if ($previousSnapshotOlderThanOneDay) {
            $this->reason = 'Previous snapshot is older than one day.';
            return true;
        }

        $previousScore = 0;
        if ($previous->getHighestSeverity() !== null) {
            $previousScore = self::SEVERITY_SCORES[strtolower($previous->getHighestSeverity())] ?? 0;
        }

        if ($previousScore < $thresholdScore) {
            $this->reason = 'Crossed threshold since previous snapshot.';
            return true;
        }

        if ($this->hasMeaningfulChange($current, $previous)) {
            $this->reason = 'Snapshot changed while still meeting/exceeding threshold.';
            return true;
        }

        $this->reason = 'No meaningful change since previous snapshot.';
        return false;
    }

    /**
     * Determine if the notification can be sent based on admin login status.
     * @return bool Whether the notification can be sent (e.g., admin is logged in).
     *
     * TODO: this is a temporary measure to prevent email sending until we have a more robust solution in place
     * for handling email notifications. In the future, this should be replaced with a more flexible and testable
     * approach that doesn't rely on admin login status.
     *
     * This differs because in specifically this case an admin needs to be logged in.
     */
    public function canSend(): bool
    {
        if (!function_exists('rsssl_admin_logged_in')) {
            return false;
        }

        $emailVerified = $this->emailService->isEmailVerified();
        if (!$emailVerified) {
            $this->reason = 'Email address not verified.';
            return false;
        }

        return (bool) rsssl_admin_logged_in();
    }

    /**
     * Get the reason for the last decision made by shouldSend().
     */
    public function getReason(): string
    {
        return $this->reasonPrefix . $this->reason;
    }

    /**
     * Check if there are meaningful changes between the current and previous snapshots.
     * A meaningful change is defined as a change in highest severity, severity score,
     * number of updatable components, or vulnerable count.
     *
     * @param VulnerabilitySnapshotDto $current
     * @param VulnerabilitySnapshotDto $previous
     * @return bool Whether there are meaningful changes.
     */
    private function hasMeaningfulChange(VulnerabilitySnapshotDto $current, VulnerabilitySnapshotDto $previous): bool
    {
        return (
            $current->getHighestSeverity() !== $previous->getHighestSeverity()
            || $current->getSeverityScore() !== $previous->getSeverityScore()
            || $current->getUpdatableComponents() !== $previous->getUpdatableComponents()
            || $current->getVulnerableCount() !== $previous->getVulnerableCount()
        );
    }
}
