<?php

declare(strict_types=1);

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

use ReallySimplePlugins\RSS\Core\Support\Helpers\Storage;

/**
 * Value object representing the persisted runtime state of a vulnerability measure.
 *
 * A "measure row" describes an action that is executed by the scheduler over time,
 * such as attempting auto-updates and eventually quarantining a component when
 * repeated attempts fail.
 *
 * Responsibilities:
 * - Validate and normalize persisted row data coming from storage.
 * - Provide a single, well-defined shape for downstream logic (no raw array access).
 * - Encapsulate simple domain rules such as "finalized" and "action time" checks.
 *
 * Design notes:
 * - Parsing is intentionally strict: invalid or incomplete rows are rejected
 *   by returning `null` from {@see fromStorage()}.
 * - This class is PHP 7.6 compatible (no readonly properties, no promoted properties).
 */
final class VulnerabilityMeasureRowDto
{
    /**
     * Keys that must exist for a row to be considered processable.
     *
     * @var string[]
     */
    private const REQUIRED_KEYS = [
        'identifier',
        'component_name',
        'component_slug',
        'type',
        'measure',
        'end_date',
        'start_date',
    ];

    /**
     * Human-readable component name (for emails, logging, UI).
     */
    private string $componentName;

    /**
     * Stable component identifier (slug) used for updates and quarantine.
     */
    private string $componentSlug;

    /**
     * Plugin file reference, required for WordPress update and quarantine actions.
     */
    private string $pluginFile;

    /**
     * Component type (plugin, theme, or core).
     */
    private string $type;

    /**
     * Selected mitigation measure (e.g. auto_update, quarantine).
     */
    private string $measure;

    /**
     * Risk level associated with the vulnerability (informational/low/medium/high/critical).
     */
    private string $risk;

    /**
     * Unique vulnerability identifier (e.g. CVE or internal ID).
     */
    private string $identifier;

    /**
     * Timestamp when the measure lifecycle started.
     */
    private int $startDate;

    /**
     * Timestamp after which the next scheduler action may be executed.
     */
    private int $endDate;

    /**
     * Number of execution attempts already performed.
     */
    private int $attempts;

    /**
     * Whether the user has been notified that this measure was scheduled.
     */
    private bool $notified;

    /**
     * Whether the measure completed successfully (e.g. update succeeded).
     */
    private bool $fixed;

    /**
     * Whether the measure was explicitly stopped after repeated failures.
     */
    private bool $stopped;

    /**
     * Whether the component was quarantined as a last-resort mitigation.
     */
    private bool $quarantined;

    /**
     * Constructor is private to guarantee creation through the factory method.
     */
    private function __construct()
    {
    }

    /**
     * Create a DTO from persisted storage data.
     *
     * Returns `null` when required fields are missing/empty or when core invariants
     * are violated. Callers are expected to silently skip such rows.
     */
    public static function fromStorage(Storage $storage): ?self
    {
        if ($storage->isOneEmpty(self::REQUIRED_KEYS)) {
            return null;
        }

        $endDate = $storage->getInt('end_date');
        if ($endDate <= 0) {
            return null;
        }

        $dto = new self();
        $dto->componentName = $storage->getString('component_name');
        $dto->componentSlug = $storage->getString('component_slug');
        $dto->pluginFile = $storage->getString('plugin_file');
        $dto->type = $storage->getString('type');
        $dto->measure = $storage->getString('measure');
        $dto->risk = $storage->getString('risk');
        $dto->identifier = $storage->getString('identifier');
        $dto->startDate = $storage->getInt('start_date');
        $dto->endDate = $endDate;
        $dto->attempts = max(0, $storage->getInt('attempts'));
        $dto->notified = $storage->getBoolean('notified');
        $dto->fixed = $storage->getBoolean('fixed');
        $dto->stopped = $storage->getBoolean('stopped');
        $dto->quarantined = $storage->getBoolean('quarantined');

        return $dto;
    }

    /**
     * Create a DTO from a raw repository array.
     * @param array<string, mixed> $row
     * @uses fromStorage()
     */
    public static function fromArray(array $row): ?self
    {
        $storage = new Storage($row);
        return self::fromStorage($storage);
    }

    /**
     * Indicates whether this measure no longer requires processing.
     *
     * A measure is considered finalized when it has been fixed, explicitly stopped,
     * or quarantined.
     */
    public function isFinalized(): bool
    {
        return $this->fixed || $this->stopped || $this->quarantined;
    }

    /**
     * Determines whether the measure is due for execution.
     */
    public function isActionAble(int $now): bool
    {
        return $this->endDate < $now;
    }

    /**
     * Export the DTO back to the repository storage format.
     *
     * @return array{
     *   component_name: string,
     *   component_slug: string,
     *   plugin_file: string,
     *   type: string,
     *   measure: string,
     *   risk: string,
     *   identifier: string,
     *   start_date: int,
     *   end_date: int,
     *   attempts: int,
     *   notified: bool,
     *   fixed: bool,
     *   stopped: bool,
     *   quarantined: bool
     * }
     */
    public function toArray(): array
    {
        return [
            'component_name' => $this->componentName,
            'component_slug' => $this->componentSlug,
            'plugin_file' => $this->pluginFile,
            'type' => $this->type,
            'measure' => $this->measure,
            'risk' => $this->risk,
            'identifier' => $this->identifier,
            'start_date' => $this->startDate,
            'end_date' => $this->endDate,
            'attempts' => $this->attempts,
            'notified' => $this->notified,
            'fixed' => $this->fixed,
            'stopped' => $this->stopped,
            'quarantined' => $this->quarantined,
        ];
    }
    public function getComponentName(): string
    {
        return $this->componentName;
    }

    public function getComponentSlug(): string
    {
        return $this->componentSlug;
    }

    public function getPluginFile(): string
    {
        return $this->pluginFile;
    }

    public function getType(): string
    {
        return $this->type;
    }

    public function getMeasure(): string
    {
        return $this->measure;
    }

    public function getIdentifier(): string
    {
        return $this->identifier;
    }

    public function getAttempts(): int
    {
        return $this->attempts;
    }

    public function isNotified(): bool
    {
        return $this->notified;
    }

    public function markNotified(): void
    {
        $this->notified = true;
    }

    /**
     * Marks this measure as successfully fixed in-memory.
     *
     * ⚠️ This method does NOT persist any changes.
     * Callers are responsible for writing the updated DTO
     * back to storage after invoking this method.
     */
    public function markFixed(): void
    {
        $this->fixed = true;
    }

    /**
     * Marks this measure as explicitly stopped in-memory.
     *
     * ⚠️ This method does NOT persist any changes.
     * Callers must persist the updated state manually.
     */
    public function markStopped(): void
    {
        $this->stopped = true;
    }

    /**
     * Marks this measure as quarantined in-memory.
     *
     * ⚠️ This method does NOT perform or persist the actual quarantine.
     * The updated DTO must be written back to storage explicitly.
     */
    public function markQuarantined(): void
    {
        $this->quarantined = true;
    }

    /**
     * Increase the execution attempt counter by one.
     *
     * Used to determine when fallback actions (stop/quarantine) should be applied.
     */
    public function incrementAttempts(): void
    {
        $this->attempts++;
    }

    /**
     * Postpone the next execution moment for this measure.
     *
     * Typically used after a failed attempt to introduce a backoff window
     * before retrying the action.
     */
    public function postponeEndDate(int $endDate): void
    {
        $this->endDate = $endDate;
    }
}
