<?php

namespace YOOtheme\Http;

use InvalidArgumentException;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;
use YOOtheme\Http\Message\Factory\Psr17Factory;

class HttpFactory
{
    protected Psr17Factory $factory;

    public function __construct()
    {
        $this->factory = new Psr17Factory();
    }

    /**
     * Creates an instance from server globals.
     *
     * @param string|UriInterface $uri
     * @param array<string, mixed> $get
     * @param array<string, mixed> $post
     * @param array<string, mixed> $files
     * @param array<string, mixed> $cookie
     * @param array<string, mixed> $server
     */
    public function createServerRequestFromGlobals(
        $uri,
        array $get = [],
        array $post = [],
        array $files = [],
        array $cookie = [],
        array $server = []
    ): Request {
        $body = fopen('php://input', 'r') ?: null;
        $method = strtoupper($server['REQUEST_METHOD'] ?? 'GET');
        $version = str_replace('HTTP/', '', $server['SERVER_PROTOCOL'] ?? '1.1');
        $headers = function_exists('getallheaders')
            ? getallheaders()
            : $this->getHeadersFromServer($server);

        $request = new Request($method, $uri, $headers, $body, $version, $server);

        if ($files = $this->normalizeFiles($files)) {
            $request = $request->withUploadedFiles($files);
        }

        if ($override = $request->getHeaderLine('X-Http-Method-Override')) {
            $request = $request->withMethod($override);
        }

        return $request->withParsedBody($post)->withQueryParams($get)->withCookieParams($cookie);
    }

    /**
     * Implementation from Laminas\Diactoros\marshalHeadersFromSapi().
     *
     * @param array<string, mixed> $server
     *
     * @return array<string, string>
     */
    public function getHeadersFromServer(array $server): array
    {
        $headers = [];

        foreach ($server as $key => $value) {
            // Apache prefixes environment variables with REDIRECT_
            // if they are added by rewrite rules
            if (str_starts_with($key, 'REDIRECT_')) {
                $key = substr($key, 9);

                // We will not overwrite existing variables with the
                // prefixed versions, though
                if (array_key_exists($key, $server)) {
                    continue;
                }
            }

            if ($value && str_starts_with($key, 'HTTP_')) {
                $name = strtr(strtolower(substr($key, 5)), '_', '-');
                $headers[$name] = $value;

                continue;
            }

            if ($value && str_starts_with($key, 'CONTENT_')) {
                $name = 'content-' . strtolower(substr($key, 8));
                $headers[$name] = $value;

                continue;
            }
        }

        return $headers;
    }

    /**
     * Return an UploadedFile instance array.
     *
     * @param array<string, mixed> $files An array which respect $_FILES structure
     *
     * @return array<string, UploadedFileInterface|list<UploadedFileInterface>>
     *
     * @throws InvalidArgumentException for unrecognized values
     */
    protected function normalizeFiles(array $files): array
    {
        $normalized = [];

        foreach ($files as $key => $value) {
            if ($value instanceof UploadedFileInterface) {
                $normalized[$key] = $value;
            } elseif (is_array($value) && isset($value['tmp_name'])) {
                $normalized[$key] = $this->createUploadedFileFromSpec($value);
            } elseif (is_array($value)) {
                $normalized[$key] = $this->normalizeFiles($value);
            } else {
                throw new InvalidArgumentException('Invalid value in files specification');
            }
        }

        return $normalized;
    }

    /**
     * Create and return an UploadedFile instance from a $_FILES specification.
     *
     * If the specification represents an array of values, this method will
     * delegate to normalizeNestedFileSpec() and return that return value.
     *
     * @param array<string, mixed> $value $_FILES struct
     *
     * @return UploadedFileInterface|list<UploadedFileInterface>
     */
    protected function createUploadedFileFromSpec(array $value)
    {
        if (is_array($value['tmp_name'])) {
            return $this->normalizeNestedFileSpec($value);
        }

        if (UPLOAD_ERR_OK !== $value['error']) {
            $stream = $this->factory->createStream();
        } else {
            try {
                $stream = $this->factory->createStreamFromFile($value['tmp_name']);
            } catch (\RuntimeException $e) {
                $stream = $this->factory->createStream();
            }
        }

        return $this->factory->createUploadedFile(
            $stream,
            (int) $value['size'],
            (int) $value['error'],
            $value['name'],
            $value['type'],
        );
    }

    /**
     * Normalize an array of file specifications.
     *
     * Loops through all nested files and returns a normalized array of
     * UploadedFileInterface instances.
     *
     * @param array<string, mixed> $files
     *
     * @return list<UploadedFileInterface>
     */
    protected function normalizeNestedFileSpec(array $files = []): array
    {
        $normalizedFiles = [];

        foreach (array_keys($files['tmp_name']) as $key) {
            $spec = [
                'tmp_name' => $files['tmp_name'][$key],
                'size' => $files['size'][$key],
                'error' => $files['error'][$key],
                'name' => $files['name'][$key],
                'type' => $files['type'][$key],
            ];
            $normalizedFiles[$key] = $this->createUploadedFileFromSpec($spec);
        }

        return $normalizedFiles;
    }
}
