<?php

declare(strict_types=1);

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

use ReallySimplePlugins\RSS\Core\Bootstrap\App;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Dtos\InstalledComponentDto;
use ReallySimplePlugins\RSS\Core\Features\Vulnerability\Interfaces\ComponentSyncStrategyInterface;
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;

/**
 * Syncs vulnerability data into storage.
 *
 * Full sync wipes all and rebuilds all.
 * Scoped sync updates only selected components.
 */
final class VulnerabilitySyncService
{
    /**
     * Allowed scoped sync types.
     */
    private const SUPPORTED_COMPONENT_TYPES = [
        'plugin' => true,
        'theme' => true,
        'core' => true,
    ];

    /**
     * @var list<ComponentSyncStrategyInterface>
     */
    private array $strategies = [];

    /**
     * Writes synced component data to 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,
        ];
    }

    /**
     * Run one whole strategy.
     *
     * Used by full sync only.
     */
    private function syncInstalledComponentsForStrategy(ComponentSyncStrategyInterface $componentSyncStrategy): void
    {
        foreach ($componentSyncStrategy->getInstalledComponents() as $installedComponentDto) {
            $this->syncInstalledComponent($componentSyncStrategy, $installedComponentDto);
        }
    }

    /**
     * Fetch, map, filter, save one component.
     *
     * This is the shared path for full and scoped sync.
     */
    private function syncInstalledComponent(
        ComponentSyncStrategyInterface $componentSyncStrategy,
        InstalledComponentDto $installedComponentDto
    ): void {
        $componentPayload = $this->fetchComponentPayloadForInstalledComponent($componentSyncStrategy, $installedComponentDto);
        if ($componentPayload === null) {
            return;
        }

        $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, 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(): void
    {
        $this->vulnerabilityStorageRepository->deleteAllComponents();

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

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

    /**
     * Sync only selected components.
     *
     * Existing stored data for other components stays untouched.
     *
     * @param list<array{type: string, slug: string}> $components
     */
    public function syncSelectedComponents(array $components): void
    {
        $selectedComponents = $this->normalizeSelectedComponents($components);
        if ($selectedComponents === []) {
            return;
        }

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

        foreach ($this->strategies as $strategy) {
            $slugsForType = $selectedComponents[$strategy->getType()] ?? [];
            if ($slugsForType === []) {
                continue;
            }

            $installedComponents = $this->getInstalledComponentsBySlug($strategy);
            foreach ($slugsForType as $slug) {
                if (!isset($installedComponents[$slug])) {
                    continue;
                }

                $this->syncInstalledComponent($strategy, $installedComponents[$slug]);
            }
        }
    }

    /**
     * Group selected slugs by type.
     *
     * Bad or duplicate rows are skipped.
     */
    private function normalizeSelectedComponents(array $components): array
    {
        $normalized = [];

        foreach ($components as $component) {
            $type = strtolower(trim((string) ($component['type'] ?? '')));
            $slug = strtolower(trim((string) ($component['slug'] ?? '')));

            if ($type === '' || $slug === '' || !isset(self::SUPPORTED_COMPONENT_TYPES[$type])) {
                continue;
            }

            if (!isset($normalized[$type])) {
                $normalized[$type] = [];
            }

            if (!in_array($slug, $normalized[$type], true)) {
                $normalized[$type][] = $slug;
            }
        }

        return $normalized;
    }

    /**
     * Index installed components by slug.
     *
     * Lets scoped sync do direct lookups per slug.
     */
    private function getInstalledComponentsBySlug(ComponentSyncStrategyInterface $componentSyncStrategy): array
    {
        $installedComponents = [];

        foreach ($componentSyncStrategy->getInstalledComponents() as $installedComponentDto) {
            $slug = strtolower(trim($installedComponentDto->getSlug()));
            if ($slug === '') {
                continue;
            }

            $installedComponents[$slug] = $installedComponentDto;
        }

        return $installedComponents;
    }
}
