<?php

declare(strict_types=1);

namespace App\Core;

/**
 * Request
 *
 * Wraps the superglobals so controllers never touch $_GET/$_POST/$_SERVER
 * directly. Every getter returns raw, unescaped data — escaping is an
 * OUTPUT concern (see helpers/functions.php::e()) so we never
 * double-escape or accidentally store HTML-encoded junk in the database.
 * Validation of input happens explicitly via App\Core\Validator, not
 * implicitly here.
 */
final class Request
{
    private array $query;
    private array $body;
    private array $server;
    private array $files;
    private array $cookies;
    private string $rawBody;
    private array $attributes = [];

    public function __construct(
        array $query = [],
        array $body = [],
        array $server = [],
        array $files = [],
        array $cookies = [],
        string $rawBody = ''
    ) {
        $this->query = $query;
        $this->body = $body;
        $this->server = $server;
        $this->files = $files;
        $this->cookies = $cookies;
        $this->rawBody = $rawBody;
    }

    public static function capture(): self
    {
        $rawBody = file_get_contents('php://input') ?: '';
        $body = $_POST;

        // Support JSON request bodies (fetch()/AJAX with Content-Type:
        // application/json) transparently alongside classic form posts.
        $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
        if ($rawBody !== '' && str_contains($contentType, 'application/json')) {
            $decoded = json_decode($rawBody, true);
            if (is_array($decoded)) {
                $body = array_merge($body, $decoded);
            }
        }

        return new self($_GET, $body, $_SERVER, $_FILES, $_COOKIE, $rawBody);
    }

    public function method(): string
    {
        $method = strtoupper($this->server['REQUEST_METHOD'] ?? 'GET');

        // Allow HTML forms (which only support GET/POST) to express
        // PUT/PATCH/DELETE via a hidden _method field, same convention
        // used by most PHP frameworks.
        if ($method === 'POST' && isset($this->body['_method'])) {
            $override = strtoupper((string) $this->body['_method']);
            if (in_array($override, ['PUT', 'PATCH', 'DELETE'], true)) {
                return $override;
            }
        }

        return $method;
    }

    public function path(): string
    {
        $uri = $this->server['REQUEST_URI'] ?? '/';
        $path = parse_url($uri, PHP_URL_PATH) ?: '/';

        return rtrim($path, '/') ?: '/';
    }

    public function isAjax(): bool
    {
        return strtolower($this->server['HTTP_X_REQUESTED_WITH'] ?? '') === 'xmlhttprequest'
            || str_contains($this->header('Accept') ?? '', 'application/json');
    }

    public function isJson(): bool
    {
        return str_contains($this->server['CONTENT_TYPE'] ?? '', 'application/json');
    }

    public function header(string $name): ?string
    {
        $key = 'HTTP_' . str_replace('-', '_', strtoupper($name));

        return $this->server[$key] ?? null;
    }

    public function bearerToken(): ?string
    {
        $auth = $this->header('Authorization') ?? '';
        if (str_starts_with($auth, 'Bearer ')) {
            return substr($auth, 7);
        }

        return null;
    }

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

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

    /** All body + query input merged, e.g. for passing to Validator::make(). */
    public function all(): array
    {
        return array_merge($this->query, $this->body);
    }

    /** Only the parsed POST/JSON body, excluding query-string params — needed for webhook signature verification (e.g. Twilio), whose signed payload is strictly the POST body, since the query string is already part of the signed URL. */
    public function bodyOnly(): array
    {
        return $this->body;
    }

    /** Lets middleware (e.g. ApiKeyAuthMiddleware) stash data for downstream controllers — the Request object is passed by reference through the middleware pipeline, so this persists for the rest of the request lifecycle. */
    public function setAttribute(string $key, mixed $value): void
    {
        $this->attributes[$key] = $value;
    }

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

    public function only(array $keys): array
    {
        return array_intersect_key($this->all(), array_flip($keys));
    }

    public function has(string $key): bool
    {
        return array_key_exists($key, $this->body) || array_key_exists($key, $this->query);
    }

    public function file(string $key): ?array
    {
        return $this->files[$key] ?? null;
    }

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

    public function ip(): string
    {
        // Trust X-Forwarded-For only if the app sits behind a known
        // reverse proxy — configure that allow-list at the web server
        // level. Falling back to REMOTE_ADDR is the safe default.
        return $this->server['REMOTE_ADDR'] ?? '0.0.0.0';
    }

    public function userAgent(): string
    {
        return substr($this->server['HTTP_USER_AGENT'] ?? '', 0, 255);
    }

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