<?php

declare(strict_types=1);

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

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\ComponentSyncStrategyInterface;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilitySnapshotRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Repositories\VulnerabilityStorageRepository;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies\CoreSyncStrategy;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies\PluginSyncStrategy;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Services\Strategies\ThemeSyncStrategy;

/**
 * Synchronize vulnerability data for installed components.
 *
 * High-level flow:
 * 1) Discover installed components via strategy classes (plugin/theme/core).
 * 2) Fetch vulnerability payloads from the API (via the strategies).
 * 3) Convert payloads into DTOs and filter vulnerabilities by installed version.
 * 4) Persist results using repositories.
 *
 * Notes:
 * - This service does not talk to the database directly; it delegates that work to repositories.
 * - Network/API calls are performed by the strategies.
 */
final class VulnerabilitySyncService
{
    /**
     * Strategies used to sync vulnerabilities per component type.
     *
     * @var list<ComponentSyncStrategyInterface>
     */
    private array $strategies = [];

    /** Persists component vulnerability data into storage. */
    private VulnerabilityStorageRepository $vulnerabilityStorageRepository;

    public function __construct(
        VulnerabilityStorageRepository $vulnerabilityStorageRepository
    ) {
        $this->vulnerabilityStorageRepository = $vulnerabilityStorageRepository;
        $this->strategies = $this->buildDefaultStrategies();
    }

    public function setStrategies(iterable $strategies): void
    {
        $this->strategies = $this->filterStrategies($strategies);
    }

    /**
     * @param iterable<ComponentSyncStrategyInterface> $strategies
     *
     * @return list<ComponentSyncStrategyInterface>
     */
    private function filterStrategies(iterable $strategies): array
    {
        $resolved = [];

        foreach ($strategies as $strategy) {
            if ($strategy instanceof ComponentSyncStrategyInterface) {
                $resolved[] = $strategy;
            }
        }

        return $resolved;
    }

    private function buildDefaultStrategies(): array
    {
        $app = App::getInstance();

        $pluginStrategy = $app->get(PluginSyncStrategy::class);

        $themeStrategy = $app->get(ThemeSyncStrategy::class);

        $coreStrategy = $app->get(CoreSyncStrategy::class);

        return [
            $pluginStrategy,
            $themeStrategy,
            $coreStrategy,
        ];
    }

    private function syncInstalledComponentsForStrategy(ComponentSyncStrategyInterface $componentSyncStrategy): void
    {
        foreach ($componentSyncStrategy->getInstalledComponents() as $installedComponentDto) {
            $componentPayload = $this->fetchComponentPayloadForInstalledComponent($componentSyncStrategy, $installedComponentDto);
            if ($componentPayload === null) {
                continue;
            }

            $dto = $componentSyncStrategy->toComponentDto($componentPayload, $installedComponentDto);

            if (!is_array($dto->vulnerabilities)) {
                $dto->vulnerabilities = [];
            }

            $installedVersion = (string) ($dto->installedVersion ?? '');
            if ($installedVersion !== '' && $dto->vulnerabilities !== []) {
                $dto->vulnerabilities = $this->filterVulnerabilitiesForInstalledVersion($installedVersion, $dto->vulnerabilities);
            }

            $this->vulnerabilityStorageRepository->saveComponent($dto);
        }
    }

    /**
     * Fetch the raw component payload for a specific installed component.
     *
     * Returns null when the API call fails or the response is not successful.
     *
     * @return array<string, mixed>|null
     */
    private function fetchComponentPayloadForInstalledComponent(ComponentSyncStrategyInterface $componentSyncStrategy, \ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto $installedComponentDto): ?array
    {
        try {
            $result = $componentSyncStrategy->fetchComponent($installedComponentDto);
        } catch (\Throwable $e) {
            $slug = $installedComponentDto->slug ?? '';
            $context = $slug !== '' ? (' (' . $slug . ')') : '';

            error_log(
                'Vulnerability API request failed for ' . $componentSyncStrategy->getType() . ' component' . $context . ': ' . $e->getMessage()
            );

            return null;
        }

        if (!isset($result['success']) || !$result['success']) {
            return null;
        }

        return $this->extractFirstComponentPayload($result['data'] ?? null);
    }

    /**
     * Extract the component payload from the API response.
     *
     * The API may return either:
     * - a single component array, or
     * - a list of components (we use the first item).
     *
     * @param mixed $data The raw `data` field from the API response.
     * @return array<string, mixed>|null The component payload or null when missing/invalid.
     */
    private function extractFirstComponentPayload($data): ?array
    {
        if (!is_array($data) || $data === []) {
            return null;
        }

        // Support both shapes:
        // 1) Single component array in $data
        // 2) List of components in $data[0]
        $payload = isset($data['slug']) ? $data : ($data[0] ?? null);

        if (!is_array($payload)) {
            return null;
        }

        return $payload;
    }

    /**
     * Filters vulnerability ranges to only those that match the installed version.
     *
     * The API provides version_from/version_to with inclusive flags. A "*" boundary
     * is treated as open-ended. Internally, version_compare() is used to evaluate
     * whether the installed version falls within each vulnerability's range.
     *
     * @param string $installedVersion Currently installed component version.
     * @param array $vulnerabilities List of VulnerabilityRangeDto objects.
     *
     * @return array List of matching VulnerabilityRangeDto objects.
     */
    private function filterVulnerabilitiesForInstalledVersion(string $installedVersion, array $vulnerabilities): array
    {
        $matches = [];

        foreach ($vulnerabilities as $vulnerability) {
            if (!is_object($vulnerability) || !method_exists($vulnerability, 'toArray')) {
                continue;
            }

            /** @var array<string, mixed> $data */
            $data = $vulnerability->toArray();

            $versionFrom = $data['version_from'];
            $versionTo = $data['version_to'];
            $fromInclusive = $data['from_inclusive'];
            $toInclusive = $data['to_inclusive'];

            if ($versionFrom !== '*') {
                $cmpFrom = version_compare($installedVersion, $versionFrom);

                if ($fromInclusive && $cmpFrom < 0) {
                    continue;
                }

                if (!$fromInclusive && $cmpFrom <= 0) {
                    continue;
                }
            }

            if ($versionTo !== '*') {
                $cmpTo = version_compare($installedVersion, $versionTo);

                if ($toInclusive && $cmpTo > 0) {
                    continue;
                }

                if (!$toInclusive && $cmpTo >= 0) {
                    continue;
                }
            }

            $matches[] = $vulnerability;
        }

        return $matches;
    }

    /**
     * Sync vulnerabilities for all supported component types.
     *
     * This clears existing stored components and writes a fresh snapshot.
     */
    public function syncAllComponents(string $trigger = ''): void
    {
        $this->vulnerabilityStorageRepository->deleteAllComponents();

        $this->strategies = $this->filterStrategies($this->strategies);
        if ($this->strategies === []) {
            return;
        }

        foreach ($this->strategies as $strategy) {
            $this->syncInstalledComponentsForStrategy($strategy);
        }
    }
}
