<?php

declare(strict_types=1);

final class MyronRemoteUpdateClient
{
    /**
     * @var null|callable(string, int): string
     */
    private $fetcher;

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

    public function __construct(?callable $fetcher = null, ?callable $poster = null)
    {
        $this->fetcher = $fetcher;
        $this->poster = $poster;
    }

    /**
     * @return array<string, mixed>
     */
    public function fetchJson(string $url, int $timeoutSeconds = 5): array
    {
        if (!$this->isHttpsUrl($url)) {
            throw new InvalidArgumentException('Remote update manifest URL must use HTTPS.');
        }

        $contents = $this->fetcher !== null
            ? (string) ($this->fetcher)($url, $timeoutSeconds)
            : $this->fetchWithStream($url, $timeoutSeconds);

        $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new RuntimeException('Remote update manifest must decode to an object.');
        }

        return $decoded;
    }

    /**
     * @param array<string, mixed> $payload
     * @return array<string, mixed>
     */
    public function postJson(string $url, array $payload, int $timeoutSeconds = 5): array
    {
        if (!$this->isHttpsUrl($url)) {
            throw new InvalidArgumentException('Remote update endpoint URL must use HTTPS.');
        }

        if ($this->poster !== null) {
            $contents = (string) ($this->poster)($url, $payload, $timeoutSeconds);
        } elseif ($this->fetcher !== null) {
            $contents = (string) ($this->fetcher)($url, $timeoutSeconds);
        } else {
            $contents = $this->postWithStream($url, $payload, $timeoutSeconds);
        }

        $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new RuntimeException('Remote update endpoint must decode to an object.');
        }

        return $decoded;
    }

    private function fetchWithStream(string $url, int $timeoutSeconds): string
    {
        $context = stream_context_create([
            'http' => [
                'method' => 'GET',
                'timeout' => max(1, $timeoutSeconds),
                'ignore_errors' => true,
                'header' => "Accept: application/json\r\nUser-Agent: MyronCMS-Update-Check\r\n",
            ],
        ]);

        $contents = @file_get_contents($url, false, $context);
        if (!is_string($contents) || trim($contents) === '') {
            throw new RuntimeException('Unable to fetch remote update manifest.');
        }

        return $contents;
    }

    /**
     * @param array<string, mixed> $payload
     */
    private function postWithStream(string $url, array $payload, int $timeoutSeconds): string
    {
        $json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
        $context = stream_context_create([
            'http' => [
                'method' => 'POST',
                'timeout' => max(1, $timeoutSeconds),
                'ignore_errors' => true,
                'header' => "Content-Type: application/json\r\nAccept: application/json\r\nUser-Agent: MyronCMS-Remote-Updater\r\n",
                'content' => $json,
            ],
        ]);

        $contents = @file_get_contents($url, false, $context);
        if (!is_string($contents) || trim($contents) === '') {
            throw new RuntimeException('Unable to fetch remote update endpoint.');
        }

        return $contents;
    }

    private function isHttpsUrl(string $url): bool
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false
            && strtolower((string) parse_url($url, PHP_URL_SCHEME)) === 'https';
    }
}
