<?php

declare(strict_types=1);

namespace Fennec\Http;

/**
 * Représente une requête HTTP entrante.
 *
 * Seule classe du framework autorisée à accéder directement aux superglobales
 * ($_SERVER, $_GET, php://input) — tout le reste du code y accède via cette abstraction.
 */
final class Request
{
    /** @var array<string, mixed> Paramètres de route résolus par le Router, attachés après construction. */
    private array $routeParameters = [];

    /** @var array<string, mixed> Attributs génériques attachés par les middlewares (ex. rôle authentifié). */
    private array $attributes = [];

    /**
     * @param array<string, string> $headers
     * @param array<string, mixed> $queryParams
     * @param array<string, mixed>|null $body Corps de la requête décodé depuis JSON, null si absent/invalide.
     */
    public function __construct(
        private readonly string $method,
        private readonly string $uri,
        private readonly array $headers,
        private readonly array $queryParams,
        private readonly ?array $body,
    ) {
    }

    /**
     * Construit une Request à partir des superglobales de la requête HTTP en cours.
     */
    public static function fromGlobals(): self
    {
        $method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
        $uri = \parse_url($_SERVER['REQUEST_URI'] ?? '/', \PHP_URL_PATH) ?? '/';
        $headers = self::extractHeaders($_SERVER);

        $rawBody = \file_get_contents('php://input');
        $decodedBody = $rawBody !== false && $rawBody !== '' ? \json_decode($rawBody, true) : null;

        return new self(
            $method,
            $uri,
            $headers,
            $_GET,
            \is_array($decodedBody) ? $decodedBody : null,
        );
    }

    public function getMethod(): string
    {
        return $this->method;
    }

    public function getUri(): string
    {
        return $this->uri;
    }

    /**
     * @return array<string, string>
     */
    public function getHeaders(): array
    {
        return $this->headers;
    }

    /**
     * Récupère un header par son nom, insensible à la casse.
     */
    public function getHeader(string $name): ?string
    {
        foreach ($this->headers as $headerName => $value) {
            if (\strcasecmp($headerName, $name) === 0) {
                return $value;
            }
        }

        return null;
    }

    /**
     * Extrait le jeton du header "Authorization: Bearer {jeton}", ou null si absent/mal formé.
     */
    public function getBearerToken(): ?string
    {
        $authorization = $this->getHeader('Authorization');

        if ($authorization === null || !\str_starts_with($authorization, 'Bearer ')) {
            return null;
        }

        return \substr($authorization, \strlen('Bearer '));
    }

    /**
     * @return array<string, mixed>
     */
    public function getQueryParams(): array
    {
        return $this->queryParams;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getBody(): ?array
    {
        return $this->body;
    }

    /**
     * Attache les paramètres de route résolus par le Router.
     *
     * @param array<string, mixed> $parameters
     */
    public function setRouteParameters(array $parameters): void
    {
        $this->routeParameters = $parameters;
    }

    public function getRouteParameter(string $name, mixed $default = null): mixed
    {
        return $this->routeParameters[$name] ?? $default;
    }

    /**
     * @return array<string, mixed>
     */
    public function getRouteParameters(): array
    {
        return $this->routeParameters;
    }

    /**
     * Attache un attribut générique à la requête (ex. rôle authentifié, posé par un middleware
     * pour être lu par un middleware suivant ou le controller).
     */
    public function setAttribute(string $name, mixed $value): void
    {
        $this->attributes[$name] = $value;
    }

    public function getAttribute(string $name, mixed $default = null): mixed
    {
        return $this->attributes[$name] ?? $default;
    }

    /**
     * @param array<string, mixed> $server
     *
     * @return array<string, string>
     */
    private static function extractHeaders(array $server): array
    {
        if (\function_exists('getallheaders')) {
            $headers = \getallheaders();

            if ($headers !== false) {
                return $headers;
            }
        }

        $headers = [];

        foreach ($server as $key => $value) {
            if (\str_starts_with($key, 'HTTP_')) {
                $name = \str_replace(' ', '-', \ucwords(\strtolower(\str_replace('_', ' ', \substr($key, 5)))));
                $headers[$name] = $value;
            }
        }

        if (isset($server['CONTENT_TYPE'])) {
            $headers['Content-Type'] = $server['CONTENT_TYPE'];
        }

        if (isset($server['CONTENT_LENGTH'])) {
            $headers['Content-Length'] = $server['CONTENT_LENGTH'];
        }

        return $headers;
    }
}
