<?php

declare(strict_types=1);

final class MyronRemoteUpdateApplyService
{
    private const MAX_REMOTE_PACKAGE_BYTES = 52428800;

    /**
     * @var null|callable(string, string, int): void
     */
    private $downloader;

    public function __construct(
        private readonly string $appRoot,
        private readonly MyronUpdateStateRepository $stateRepository,
        private readonly MyronUpdateApplicationService $updateService,
        ?callable $downloader = null,
    ) {
        $this->downloader = $downloader;
    }

    /**
     * @param array<string, mixed> $actor
     * @return array<string, mixed>
     */
    public function applyAvailableUpdate(array $actor): array
    {
        if (!$this->stateRepository->hasVisibleAvailableUpdate()) {
            throw new RuntimeException('No remote application update is available.');
        }

        $state = $this->stateRepository->get();
        $availableUpdate = is_array($state['available_update'] ?? null) ? $state['available_update'] : null;
        if ($availableUpdate === null) {
            throw new RuntimeException('No remote application update metadata is available.');
        }

        $package = is_array($availableUpdate['package'] ?? null) ? $availableUpdate['package'] : [];
        $url = trim((string) ($package['url'] ?? ''));
        $checksum = strtolower(trim((string) ($package['checksumSha256'] ?? '')));
        $declaredSize = (int) ($package['sizeBytes'] ?? 0);
        if (empty($package['gated'])) {
            throw new RuntimeException('Remote update package must be delivered through a gated download URL.');
        }
        $this->assertAvailableUpdateStillEntitled($availableUpdate);
        $this->assertRemotePackageMetadata($url, $checksum, $declaredSize);

        $downloadRoot = $this->appRoot . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'updates' . DIRECTORY_SEPARATOR . 'remote';
        $this->ensureDirectory($downloadRoot);
        if (!is_writable($downloadRoot)) {
            throw new RuntimeException('Remote update download workspace is not writable: app/storage/updates/remote');
        }

        $zipPath = $downloadRoot . DIRECTORY_SEPARATOR . 'remote-update-' . bin2hex(random_bytes(8)) . '.zip';
        try {
            $this->downloadPackage($url, $zipPath, 30);
            $actualSize = filesize($zipPath);
            if (!is_int($actualSize) || $actualSize <= 0) {
                throw new RuntimeException('Remote update package download is empty.');
            }

            if ($declaredSize > 0 && $actualSize !== $declaredSize) {
                throw new RuntimeException('Remote update package size does not match the update manifest.');
            }

            $actualChecksum = hash_file('sha256', $zipPath);
            if (!is_string($actualChecksum) || !hash_equals($checksum, strtolower($actualChecksum))) {
                throw new RuntimeException('Remote update package checksum does not match the update manifest.');
            }

            $result = $this->updateService->applyPackagePath($zipPath, (string) ($actor['id'] ?? ''));
            $this->stateRepository->recordAppliedUpdate($result);

            return $result;
        } finally {
            if (is_file($zipPath)) {
                unlink($zipPath);
            }
        }
    }

    private function assertRemotePackageMetadata(string $url, string $checksum, int $declaredSize): void
    {
        if (filter_var($url, FILTER_VALIDATE_URL) === false || strtolower((string) parse_url($url, PHP_URL_SCHEME)) !== 'https') {
            throw new RuntimeException('Remote update package URL must use HTTPS.');
        }

        if (!preg_match('/^[a-f0-9]{64}$/', $checksum)) {
            throw new RuntimeException('Remote update package checksum is invalid.');
        }

        if ($declaredSize < 0 || $declaredSize > self::MAX_REMOTE_PACKAGE_BYTES) {
            throw new RuntimeException('Remote update package size is outside the allowed limit.');
        }
    }

    /**
     * @param array<string, mixed> $availableUpdate
     */
    private function assertAvailableUpdateStillEntitled(array $availableUpdate): void
    {
        $policy = is_array($availableUpdate['licensePolicy'] ?? null)
            ? $availableUpdate['licensePolicy']
            : (is_array($availableUpdate['license_policy'] ?? null) ? $availableUpdate['license_policy'] : []);
        $capability = trim((string) (
            $availableUpdate['requiredCapability']
            ?? $availableUpdate['required_capability']
            ?? $policy['requiredCapability']
            ?? $policy['required_capability']
            ?? ''
        ));
        if ($capability === '' || !function_exists('myronEntitlementGate')) {
            return;
        }

        try {
            $decision = myronEntitlementGate()->evaluate($capability);
        } catch (Throwable) {
            $this->stateRepository->recordCheck('entitlement_denied', null, 'Remote update package is not available for the current license.', false);
            throw new RuntimeException('Remote update package is not available for the current license.');
        }
        if (!in_array((string) ($decision['outcome'] ?? ''), ['allowed', 'warn'], true)) {
            $this->stateRepository->recordCheck('entitlement_denied', null, 'Remote update package is not available for the current license.', false);
            throw new RuntimeException('Remote update package is not available for the current license.');
        }
    }

    private function downloadPackage(string $url, string $destination, int $timeoutSeconds): void
    {
        if ($this->downloader !== null) {
            ($this->downloader)($url, $destination, $timeoutSeconds);
            return;
        }

        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'timeout' => max(1, $timeoutSeconds),
                'ignore_errors' => false,
                'header' => "Accept: application/zip\r\nUser-Agent: MyronCMS-Remote-Updater\r\n",
            ],
        ]);
        $contents = @file_get_contents($url, false, $context);
        if (!is_string($contents) || $contents === '') {
            throw new RuntimeException('Unable to download remote update package.');
        }

        if (strlen($contents) > self::MAX_REMOTE_PACKAGE_BYTES) {
            throw new RuntimeException('Remote update package exceeds the allowed size limit.');
        }

        if (file_put_contents($destination, $contents, LOCK_EX) === false) {
            throw new RuntimeException('Unable to write remote update package to the update workspace.');
        }
    }

    private function ensureDirectory(string $path): void
    {
        if (!is_dir($path) && !mkdir($path, 0775, true) && !is_dir($path)) {
            throw new RuntimeException('Unable to create directory: ' . $path);
        }
    }
}
