<?php

declare(strict_types=1);

namespace App\Framework\AsyncExamples\Http;

use App\Framework\Async\AsyncPool;
use App\Framework\Async\FiberManager;
use Fiber;

/**
 * Asynchroner HTTP-Client mit Fiber-Unterstützung
 */
final class AsyncHttpClient
{
    /** @var array<string, mixed> */
    private array $defaultOptions = [
        'timeout' => 30,
        'connect_timeout' => 10,
        'follow_redirects' => true,
        'max_redirects' => 5,
        'user_agent' => 'AsyncHttpClient/1.0',
    ];

    public function __construct(
        private readonly FiberManager $fiberManager = new FiberManager(),
        /** @var array<string, mixed> */
        array $defaultOptions = []
    ) {
        $this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions);
    }

    /**
     * Sendet einen GET-Request
     */
    public function get(string $url, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
    {
        return $this->request('GET', $url, null, $headers, $options);
    }

    /**
     * Sendet einen POST-Request
     */
    public function post(string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
    {
        return $this->request('POST', $url, $data, $headers, $options);
    }

    /**
     * Sendet einen PUT-Request
     */
    public function put(string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
    {
        return $this->request('PUT', $url, $data, $headers, $options);
    }

    /**
     * Sendet einen DELETE-Request
     */
    public function delete(string $url, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
    {
        return $this->request('DELETE', $url, null, $headers, $options);
    }

    /**
     * Sendet mehrere Requests parallel
     *
     * @param array<string, array<string, mixed>> $requests ['key' => ['method' => 'GET', 'url' => '...', ...]]
     * @return array<string, HttpResponse>
     */
    public function requestMultiple(array $requests): array
    {
        /** @var array<string, \Closure> */
        $operations = [];
        foreach ($requests as $key => $request) {
            $operations[$key] = fn () => $this->request(
                $request['method'] ?? 'GET',
                $request['url'],
                $request['data'] ?? null,
                $request['headers'] ?? [],
                $request['options'] ?? []
            );
        }

        return $this->fiberManager->batch($operations);
    }

    /**
     * Sendet Requests mit begrenzter Parallelität
     *
     * @param array<string, array<string, mixed>> $requests
     * @return array<string, HttpResponse>
     */
    public function requestBatch(array $requests, int $maxConcurrency = 10): array
    {
        $pool = new AsyncPool($maxConcurrency, $this->fiberManager);

        foreach ($requests as $key => $request) {
            $pool->add(
                fn () => $this->request(
                    $request['method'] ?? 'GET',
                    $request['url'],
                    $request['data'] ?? null,
                    $request['headers'] ?? [],
                    $request['options'] ?? []
                ),
                $key
            );
        }

        return $pool->execute();
    }

    /**
     * Hauptmethode für HTTP-Requests
     */
    private function request(string $method, string $url, mixed $data = null, /** @var array<string, string> */ array $headers = [], /** @var array<string, mixed> */ array $options = []): HttpResponse
    {
        $options = array_merge($this->defaultOptions, $options);

        $context = $this->createContext($method, $data, $headers, $options);

        $startTime = microtime(true);

        try {
            $content = @file_get_contents($url, false, $context);

            if ($content === false) {
                $error = error_get_last();

                throw new HttpException("HTTP request failed: " . ($error['message'] ?? 'Unknown error'));
            }

            $responseHeaders = $this->parseHeaders($http_response_header ?? []);
            $statusCode = $this->extractStatusCode($http_response_header ?? []);

            return new HttpResponse(
                statusCode: $statusCode,
                headers: $responseHeaders,
                body: $content,
                requestTime: microtime(true) - $startTime
            );

        } catch (\Throwable $e) {
            throw new HttpException("HTTP request failed: " . $e->getMessage(), 0, $e);
        }
    }

    /**
     * @return resource
     */
    private function createContext(string $method, mixed $data, /** @var array<string, string> */ array $headers, /** @var array<string, mixed> */ array $options)
    {
        /** @var array<string, array<string, mixed>> */
        $contextOptions = [
            'http' => [
                'method' => $method,
                'timeout' => $options['timeout'],
                'user_agent' => $options['user_agent'],
                'follow_location' => $options['follow_redirects'],
                'max_redirects' => $options['max_redirects'],
                'ignore_errors' => true,
            ],
        ];

        if ($data !== null) {
            if (is_array($data) || is_object($data)) {
                $contextOptions['http']['content'] = json_encode($data);
                $headers['Content-Type'] = 'application/json';
            } else {
                $contextOptions['http']['content'] = (string)$data;
            }
        }

        if (! empty($headers)) {
            /** @var array<int, string> */
            $headerStrings = [];
            foreach ($headers as $key => $value) {
                $headerStrings[] = "$key: $value";
            }
            $contextOptions['http']['header'] = implode("\r\n", $headerStrings);
        }

        return stream_context_create($contextOptions);
    }

    /**
     * @param array<int, string> $httpResponseHeader
     * @return array<string, string>
     */
    private function parseHeaders(array $httpResponseHeader): array
    {
        /** @var array<string, string> */
        $headers = [];
        foreach ($httpResponseHeader as $header) {
            if (strpos($header, ':') !== false) {
                [$key, $value] = explode(':', $header, 2);
                $headers[trim($key)] = trim($value);
            }
        }

        return $headers;
    }

    /**
     * @param array<int, string> $httpResponseHeader
     */
    private function extractStatusCode(array $httpResponseHeader): int
    {
        if (empty($httpResponseHeader)) {
            return 0;
        }

        $statusLine = $httpResponseHeader[0];
        if (preg_match('/HTTP\/\d+\.\d+\s+(\d+)/', $statusLine, $matches)) {
            return (int)$matches[1];
        }

        return 0;
    }
}
