<?php
declare(strict_types=1);

namespace Infolab;

/**
 * Façade lecture seule sur la requête HTTP courante.
 * Parse les headers, le body JSON (méthodes non-GET) et expose les query params.
 */
final class Request
{
    /** @var array<string,mixed> */
    private array $jsonBody;

    /** @var array<string,mixed> */
    private array $query;

    /** @var array<string,string> headers en clés lowercase */
    private array $headers;

    public function __construct()
    {
        $this->query   = $_GET;
        $this->headers = $this->loadHeaders();
        $this->jsonBody = $this->loadJsonBody();
    }

    /** @return array<string,string> */
    private function loadHeaders(): array
    {
        $h = [];
        // getallheaders() existe avec Apache et le serveur intégré PHP récent.
        if (function_exists('getallheaders')) {
            $all = getallheaders();
            if (is_array($all)) {
                foreach ($all as $k => $v) {
                    $h[strtolower((string) $k)] = (string) $v;
                }
                return $h;
            }
        }
        // Fallback : reconstruit depuis $_SERVER (CGI/FPM).
        foreach ($_SERVER as $k => $v) {
            if (is_string($k) && str_starts_with($k, 'HTTP_')) {
                $name = strtolower(str_replace('_', '-', substr($k, 5)));
                $h[$name] = (string) $v;
            }
        }
        // Content-Type / Content-Length ne sont pas préfixés HTTP_
        if (isset($_SERVER['CONTENT_TYPE']))   $h['content-type']   = (string) $_SERVER['CONTENT_TYPE'];
        if (isset($_SERVER['CONTENT_LENGTH'])) $h['content-length'] = (string) $_SERVER['CONTENT_LENGTH'];
        return $h;
    }

    /** @return array<string,mixed> */
    private function loadJsonBody(): array
    {
        $method = $this->method();
        if (!in_array($method, ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
            return [];
        }
        $raw = file_get_contents('php://input');
        if ($raw === false || $raw === '') {
            return [];
        }
        $ct = $this->header('content-type', '') ?? '';
        if ($ct !== '' && !str_contains(strtolower($ct), 'application/json')) {
            // Body non-JSON : on n'essaie pas de parser. Les contrôleurs traiteront comme vide.
            return [];
        }
        try {
            $decoded = json_decode($raw, true, 64, JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            throw new \ApiException(400, ['error' => 'bad_request', 'message' => 'invalid JSON body']);
        }
        return is_array($decoded) ? $decoded : [];
    }

    public function method(): string
    {
        return strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
    }

    /** Chemin sans query string. */
    public function path(): string
    {
        $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
        $q = strpos($uri, '?');
        return $q === false ? $uri : substr($uri, 0, $q);
    }

    public function header(string $name, ?string $default = null): ?string
    {
        return $this->headers[strtolower($name)] ?? $default;
    }

    /** Extrait le token Bearer de l'header Authorization, ou null. */
    public function bearerToken(): ?string
    {
        $auth = $this->header('authorization');
        if ($auth === null) {
            return null;
        }
        if (preg_match('/^Bearer\s+(.+)$/i', $auth, $m)) {
            return trim($m[1]);
        }
        return null;
    }

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

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

    /** @return array<string,mixed> payload JSON décodé (vide si pas de body) */
    public function json(): array
    {
        return $this->jsonBody;
    }

    public function input(string $key, mixed $default = null): mixed
    {
        return $this->jsonBody[$key] ?? $default;
    }

    // -------------------------------------------------------------------------
    // multipart/form-data — utilisé par les endpoints d'upload (galerie images).
    // PHP peuple lui-même $_POST et $_FILES quand le Content-Type est multipart,
    // on se contente d'exposer ça proprement.
    // -------------------------------------------------------------------------

    public function isMultipart(): bool
    {
        $ct = $this->header('content-type', '') ?? '';
        return str_contains(strtolower($ct), 'multipart/form-data');
    }

    /**
     * Récupère un champ du form (multipart ou x-www-form-urlencoded).
     */
    public function formField(string $name, ?string $default = null): ?string
    {
        $v = $_POST[$name] ?? null;
        return $v === null ? $default : (string) $v;
    }

    /**
     * Récupère un fichier uploadé. Retourne null si absent ou si erreur d'upload.
     *
     * @return array{name:string,type:string,tmp_name:string,error:int,size:int}|null
     */
    public function file(string $name): ?array
    {
        $f = $_FILES[$name] ?? null;
        if (!is_array($f) || !isset($f['error']) || $f['error'] === UPLOAD_ERR_NO_FILE) {
            return null;
        }
        return $f;
    }
}

