<?php

declare(strict_types=1);

final class MyronRemoteUpdateCheckService
{
    public function __construct(
        private readonly MyronUpdateSettingsRepository $settingsRepository,
        private readonly MyronUpdateStateRepository $stateRepository,
        private readonly MyronRemoteUpdateClient $client,
        private readonly MyronRemoteUpdateManifestValidator $manifestValidator,
        private readonly MyronVersionComparator $versionComparator,
        private readonly ?MyronLicenseRepository $licenseRepository = null,
    ) {
    }

    /**
     * @return array<string, mixed>
     */
    public function checkNow(bool $manual = false): array
    {
        $settings = $this->settingsRepository->get();
        if ($manual && !$this->manualCooldownHasElapsed($settings)) {
            return [
                'status' => 'manual_check_cooldown',
                'availableUpdate' => null,
                'message' => 'Please wait before checking for updates again.',
            ];
        }

        try {
            $endpoint = (string) ($settings['manifest_url'] ?? '');
            $manifest = str_contains($endpoint, '/api/v1/check')
                ? $this->mediatedManifest($endpoint, $settings)
                : $this->client->fetchJson($endpoint, 5);
            $validated = $this->manifestValidator->validate(
                $manifest,
                (string) ($settings['update_channel'] ?? 'beta'),
                MyronReleaseMetadata::VERSION
            );

            if ($this->versionComparator->isNewer((string) $validated['latestVersion'], MyronReleaseMetadata::VERSION)) {
                if (trim((string) ($validated['package']['url'] ?? '')) === '') {
                    $this->stateRepository->recordCheck('entitlement_denied', null, 'installable_package_not_available', $manual);
                    return [
                        'status' => 'entitlement_denied',
                        'availableUpdate' => null,
                        'message' => 'This update is not available for installation on the current license.',
                    ];
                }
                $this->stateRepository->recordCheck('update_available', $validated, '', $manual);
                return [
                    'status' => 'update_available',
                    'availableUpdate' => $validated,
                    'message' => 'MyronCMS ' . (string) $validated['latestVersion'] . ' is available.',
                ];
            }

            $this->stateRepository->recordCheck('up_to_date', null, '', $manual);
            return [
                'status' => 'up_to_date',
                'availableUpdate' => null,
                'message' => 'MyronCMS is up to date.',
            ];
        } catch (Throwable $exception) {
            $message = $exception->getMessage();
            $status = str_starts_with($message, 'Update requires ')
                ? 'incompatible_update'
                : (in_array($message, ['entitlement_denied', 'license_refresh_required'], true) ? $message : 'check_failed');
            $this->stateRepository->recordCheck($status, null, $exception->getMessage(), $manual);
            return [
                'status' => $status,
                'availableUpdate' => null,
                'message' => $exception->getMessage(),
            ];
        }
    }

    /**
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function mediatedManifest(string $endpoint, array $settings): array
    {
        $response = $this->client->postJson($endpoint, $this->updateCheckPayload((string) ($settings['update_channel'] ?? 'beta')), 5);
        if (isset($response['product'], $response['manifest_version'])) {
            return $response;
        }
        if (($response['ok'] ?? false) !== true) {
            throw new RuntimeException('check_failed');
        }

        $status = (string) ($response['status'] ?? '');
        if ($status === 'entitlement_denied') {
            $reason = trim((string) ($response['reasonCode'] ?? $response['decision']['reasonCode'] ?? 'entitlement_denied'));
            throw new RuntimeException($reason === 'activation_not_found' ? 'license_refresh_required' : 'entitlement_denied');
        }

        $manifest = is_array($response['manifest'] ?? null) ? $response['manifest'] : $response;
        if (!is_array($manifest)) {
            throw new RuntimeException('check_failed');
        }

        return $manifest;
    }

    /**
     * @return array<string, mixed>
     */
    private function updateCheckPayload(string $channel): array
    {
        $row = $this->licenseRepository?->current() ?? [];
        $snapshot = $this->licenseRepository?->currentSnapshot();
        $host = strtolower((string) parse_url(myronAppUrl(), PHP_URL_HOST));
        return [
            'activationId' => (string) ($row['activation_id'] ?? ''),
            'licenseFingerprint' => (string) ($row['license_fingerprint'] ?? ''),
            'plan' => $snapshot !== null ? $snapshot->plan : 'core',
            'licenseStatus' => $snapshot !== null ? $snapshot->status : 'missing',
            'capabilities' => $snapshot !== null ? $snapshot->capabilities : [],
            'deploymentGroup' => $snapshot !== null ? $snapshot->deploymentGroup : '',
            'environment' => $snapshot !== null ? $snapshot->environment : 'production',
            'installedVersion' => MyronReleaseMetadata::VERSION,
            'phpVersion' => PHP_VERSION,
            'channel' => $channel,
            'siteUrl' => myronAppUrl(),
            'host' => $host,
            'updateClass' => 'feature_update',
            'integrity' => function_exists('myronEntitlementBoundaryIntegrityService') ? myronEntitlementBoundaryIntegrityService()->snapshot() : ['status' => 'ok'],
        ];
    }

    /**
     * @return array<string, mixed>
     */
    public function checkIfStale(): array
    {
        $settings = $this->settingsRepository->get();
        if (empty($settings['auto_check_enabled'])) {
            return [
                'status' => 'skipped',
                'message' => 'Automatic update checks are disabled.',
            ];
        }

        $state = $this->stateRepository->get();
        $lastCheckedAt = (string) ($state['last_checked_at'] ?? '');
        $intervalSeconds = max(1, (int) ($settings['auto_check_interval_hours'] ?? 24)) * 3600;
        $lastCheckedTimestamp = $this->storedUtcTimestamp($lastCheckedAt);
        if ($lastCheckedTimestamp !== null && time() - $lastCheckedTimestamp < $intervalSeconds) {
            return [
                'status' => 'not_stale',
                'message' => 'Update check is not stale.',
            ];
        }

        return $this->checkNow(false);
    }

    /**
     * @param array<string, mixed> $settings
     */
    private function manualCooldownHasElapsed(array $settings): bool
    {
        $state = $this->stateRepository->get();
        $lastManual = (string) ($state['last_manual_checked_at'] ?? '');
        $cooldownSeconds = max(0, (int) ($settings['manual_check_cooldown_seconds'] ?? 60));
        $lastManualTimestamp = $this->storedUtcTimestamp($lastManual);
        if ($lastManualTimestamp === null || $cooldownSeconds === 0) {
            return true;
        }

        return time() - $lastManualTimestamp >= $cooldownSeconds;
    }

    private function storedUtcTimestamp(string $value): ?int
    {
        $trimmed = trim($value);
        if ($trimmed === '') {
            return null;
        }

        $date = DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $trimmed, new DateTimeZone('UTC'));
        if ($date instanceof DateTimeImmutable) {
            return $date->getTimestamp();
        }

        return null;
    }
}
