<?php

declare(strict_types=1);

final class MyronRemoteModulePackageClient
{
    private const MAX_REMOTE_PACKAGE_BYTES = 52428800;

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

    public function __construct(
        private readonly MyronRemoteUpdateClient $client,
        private readonly ?MyronLicenseRepository $licenseRepository,
        private readonly MyronEntitlementBoundaryIntegrityService $integrityService,
        private readonly string $appRoot,
        private readonly string $endpointUrl = '',
        ?callable $downloader = null,
    ) {
        $this->downloader = $downloader;
    }

    /**
     * @return array{status:string,path:string,manifest?:array<string,mixed>,reason?:string}
     */
    public function downloadPackageForSku(string $sku): array
    {
        $available = $this->inspectPackageForSku($sku);
        if (($available['status'] ?? '') !== 'module_package_available') {
            return $available + ['path' => ''];
        }
        $manifest = is_array($available['manifest'] ?? null) ? $available['manifest'] : [];
        $package = is_array($manifest['package'] ?? null) ? $manifest['package'] : [];
        $url = trim((string) ($package['url'] ?? ''));
        $checksum = strtolower(trim((string) ($package['checksum_sha256'] ?? $package['checksumSha256'] ?? '')));
        $size = (int) ($package['size_bytes'] ?? $package['sizeBytes'] ?? 0);
        if (empty($package['gated']) || filter_var($url, FILTER_VALIDATE_URL) === false || strtolower((string) parse_url($url, PHP_URL_SCHEME)) !== 'https') {
            return ['status' => 'remote_package_invalid', 'path' => '', 'manifest' => $manifest];
        }
        if (!preg_match('/^[a-f0-9]{64}$/', $checksum)) {
            return ['status' => 'remote_package_invalid', 'path' => '', 'manifest' => $manifest];
        }

        $downloadRoot = rtrim($this->appRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'module-packages' . DIRECTORY_SEPARATOR . 'downloads';
        $this->ensureDirectory($downloadRoot);
        $zipPath = $downloadRoot . DIRECTORY_SEPARATOR . 'module-' . preg_replace('/[^a-z0-9_-]/', '-', strtolower($sku)) . '-' . bin2hex(random_bytes(8)) . '.zip';
        $this->download($url, $zipPath, 30);
        $actualSize = filesize($zipPath);
        if (!is_int($actualSize) || $actualSize <= 0 || $actualSize > self::MAX_REMOTE_PACKAGE_BYTES || ($size > 0 && $actualSize !== $size)) {
            @unlink($zipPath);
            return ['status' => 'remote_package_size_mismatch', 'path' => '', 'manifest' => $manifest];
        }
        $actualChecksum = hash_file('sha256', $zipPath);
        if (!is_string($actualChecksum) || !hash_equals($checksum, strtolower($actualChecksum))) {
            @unlink($zipPath);
            return ['status' => 'remote_package_checksum_mismatch', 'path' => '', 'manifest' => $manifest];
        }

        return ['status' => 'downloaded', 'path' => $zipPath, 'manifest' => $manifest];
    }

    /**
     * @return array{status:string,path:string,manifest?:array<string,mixed>,latestVersion?:string,reason?:string}
     */
    public function inspectPackageForSku(string $sku): array
    {
        $endpoint = $this->endpoint();
        if ($endpoint === '') {
            return ['status' => 'remote_package_endpoint_not_configured', 'path' => ''];
        }

        try {
            $response = $this->client->postJson($endpoint, $this->payload($sku), 8);
        } catch (Throwable) {
            return ['status' => 'remote_package_unavailable', 'path' => '', 'reason' => 'remote_package_unavailable'];
        }
        $status = (string) ($response['status'] ?? '');
        if (($response['ok'] ?? false) !== true || $status !== 'module_package_available') {
            return [
                'status' => $status !== '' ? $status : 'remote_package_unavailable',
                'path' => '',
                'reason' => (string) ($response['reasonCode'] ?? $response['code'] ?? 'remote_package_unavailable'),
            ];
        }

        $manifest = is_array($response['manifest'] ?? null) ? $response['manifest'] : [];
        $latestVersion = trim((string) ($manifest['latest_version'] ?? $manifest['latestVersion'] ?? $manifest['version'] ?? ''));
        return [
            'status' => 'module_package_available',
            'path' => '',
            'manifest' => $manifest,
            'latestVersion' => $latestVersion,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function payload(string $sku): array
    {
        $row = $this->licenseRepository?->current() ?? [];
        $snapshot = $this->licenseRepository?->currentSnapshot();
        $siteUrl = function_exists('myronAppUrl') ? myronAppUrl() : '';
        $host = strtolower((string) parse_url($siteUrl, PHP_URL_HOST));
        $installedPackageVersion = function_exists('myronModulePackageInstallLedger')
            ? myronModulePackageInstallLedger()->installedVersionForSku($sku)
            : '';
        return [
            'activationId' => (string) ($row['activation_id'] ?? ''),
            'licenseFingerprint' => (string) ($row['license_fingerprint'] ?? ''),
            'deploymentGroup' => $snapshot !== null ? $snapshot->deploymentGroup : '',
            'environment' => $snapshot !== null ? $snapshot->environment : 'production',
            'installedVersion' => class_exists(MyronReleaseMetadata::class) ? MyronReleaseMetadata::VERSION : '',
            'installedPackageVersion' => $installedPackageVersion,
            'targetPackage' => $sku,
            'siteUrl' => $siteUrl,
            'host' => $host,
            'integrity' => $this->integrityService->snapshot(),
        ];
    }

    private function endpoint(): string
    {
        $configured = trim($this->endpointUrl);
        if ($configured !== '') {
            return $configured;
        }
        $env = trim((string) getenv('MYRON_MODULE_PACKAGE_API_URL'));
        if ($env !== '') {
            return $env;
        }
        $base = trim((string) getenv('MYRON_UPDATE_API_URL'));
        if ($base !== '') {
            return preg_replace('#/check\\.php$#', '/module-package.php', $base) ?? $base;
        }

        return 'https://updates.myroncms.com/api/v1/module-package.php';
    }

    private function download(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-Module-Package\r\n",
            ],
        ]);
        $contents = @file_get_contents($url, false, $context);
        if (!is_string($contents) || $contents === '') {
            throw new RuntimeException('Unable to download remote module package.');
        }
        if (strlen($contents) > self::MAX_REMOTE_PACKAGE_BYTES) {
            throw new RuntimeException('Remote module package exceeds the allowed size limit.');
        }
        if (file_put_contents($destination, $contents, LOCK_EX) === false) {
            throw new RuntimeException('Unable to write remote module package.');
        }
    }

    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);
        }
    }
}
