<?php

namespace App\Core;

/**
 * Request — HTTP Request Abstraction
 *
 * Wraps PHP superglobals ($_GET, $_POST, $_SERVER) into a single,
 * immutable-by-convention object. Named route parameters are injected
 * by the Router after pattern matching.
 *
 * Design constraints:
 *   - No external dependencies.
 *   - Not truly immutable (setParams() is required by Router).
 *     All other properties are read-only via accessor methods.
 *   - Base path stripping ensures routes work in subdirectory installs
 *     (XAMPP: /inveetaire/public/) and at the domain root (production).
 *
 * Usage:
 *   $request = Request::fromGlobals();
 *   $request->getMethod();          // 'GET' | 'POST'
 *   $request->getPath();            // '/login', '/app/couple/guests'
 *   $request->get('search');        // ?search= query value
 *   $request->post('email');        // POST body value
 *   $request->param('id');          // Named route param {id}
 *
 * @package Inveetaire
 * @ref     DBP v1.0 Ch.5, SAD v1.1 Ch.4, INIT-007
 */
class Request
{
    // ─────────────────────────────────────────────────────────────────────────
    // PROPERTIES
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * HTTP method — always uppercase.
     * Inveetaire only accepts GET and POST (DBP v1.0 Ch.5).
     * @var string
     */
    private string $method;

    /**
     * Application-relative URI path with leading slash, no query string.
     * Always stripped of the subdirectory prefix.
     * Examples: '/', '/login', '/app/couple/guests', '/i/{slug}/rsvp/{code}'
     * @var string
     */
    private string $path;

    /**
     * Raw $_GET query parameters.
     * @var array<string, mixed>
     */
    private array $queryParams;

    /**
     * Raw $_POST body parameters.
     * @var array<string, mixed>
     */
    private array $postParams;

    /**
     * Raw $_SERVER superglobal.
     * @var array<string, mixed>
     */
    private array $server;

    /**
     * Named URL parameters extracted by the Router after pattern matching.
     * Populated by Router::dispatch() via setParams().
     * e.g. ['id' => '42', 'slug' => 'smith-wedding']
     * @var array<string, string>
     */
    private array $routeParams = [];

    // ─────────────────────────────────────────────────────────────────────────
    // CONSTRUCTOR
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * @param string              $method      HTTP method (GET|POST).
     * @param string              $path        App-relative URI path.
     * @param array<string,mixed> $queryParams $_GET values.
     * @param array<string,mixed> $postParams  $_POST values.
     * @param array<string,mixed> $server      $_SERVER values.
     */
    public function __construct(
        string $method,
        string $path,
        array  $queryParams,
        array  $postParams,
        array  $server
    ) {
        $this->method      = strtoupper($method);
        $this->path        = $path;
        $this->queryParams = $queryParams;
        $this->postParams  = $postParams;
        $this->server      = $server;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // FACTORY
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Create a Request from PHP superglobals.
     * Called once by Application::handleRequest() → Router::dispatch().
     *
     * Base path stripping:
     *   SCRIPT_NAME is used to detect the subdirectory prefix.
     *   Example: SCRIPT_NAME = /inveetaire/public/index.php
     *            basePath    = /inveetaire/public
     *            REQUEST_URI = /inveetaire/public/login?ref=email
     *            path        = /login
     *
     *   In production at the domain root, basePath will be '' or '/'
     *   and stripping is a safe no-op.
     *
     * @return self
     */
    public static function fromGlobals(): self
    {
        $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
        $uri    = $_SERVER['REQUEST_URI'] ?? '/';

        // Strip query string.
        $path = strtok($uri, '?') ?: '/';

        // Detect the application base path.
        $basePath = base_path();

        // Strip the base path prefix so routes are always application-relative.
        if ($basePath !== '' && strpos($path, $basePath) === 0) {
            $path = substr($path, strlen($basePath));
        }

        // Ensure the path always starts with a slash.
        $path = '/' . ltrim($path, '/');

        // Strip trailing slash except for root '/'.
        if ($path !== '/' && substr($path, -1) === '/') {
            $path = rtrim($path, '/');
        }

        return new self($method, $path, $_GET, $_POST, $_SERVER);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // METHOD + PATH
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return the HTTP method in uppercase.
     * @return string  'GET' or 'POST'
     */
    public function getMethod(): string
    {
        return $this->method;
    }

    /**
     * Return the application-relative URI path.
     * No trailing slash. Always starts with '/'.
     * @return string  e.g. '/login', '/app/couple/guests'
     */
    public function getPath(): string
    {
        return $this->path;
    }

    /**
     * Return true if the HTTP method is GET.
     * @return bool
     */
    public function isGet(): bool
    {
        return $this->method === 'GET';
    }

    /**
     * Return true if the HTTP method is POST.
     * @return bool
     */
    public function isPost(): bool
    {
        return $this->method === 'POST';
    }

    // ─────────────────────────────────────────────────────────────────────────
    // QUERY PARAMS ($_GET)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return a query parameter value (?key=value).
     *
     * @param string $key     Query parameter name.
     * @param mixed  $default Value returned when the key is absent.
     * @return mixed
     */
    public function get(string $key, $default = null)
    {
        return $this->queryParams[$key] ?? $default;
    }

    /**
     * Return all query parameters as an associative array.
     * @return array<string, mixed>
     */
    public function allGet(): array
    {
        return $this->queryParams;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // POST PARAMS ($_POST)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return a POST body parameter value.
     *
     * @param string $key     Parameter name.
     * @param mixed  $default Value returned when the key is absent.
     * @return mixed
     */
    public function post(string $key, $default = null)
    {
        return $this->postParams[$key] ?? $default;
    }

    /**
     * Return all POST parameters as an associative array.
     * @return array<string, mixed>
     */
    public function allPost(): array
    {
        return $this->postParams;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // SERVER
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Return a value from $_SERVER.
     *
     * @param string $key     Server variable name (e.g. 'REMOTE_ADDR').
     * @param mixed  $default Value returned when the key is absent.
     * @return mixed
     */
    public function server(string $key, $default = null)
    {
        return $this->server[$key] ?? $default;
    }

    /**
     * Return an HTTP request header value.
     * Header names are case-insensitive; dashes are converted to underscores.
     *
     * @param string $name    Header name (e.g. 'Content-Type', 'X-CSRF-Token').
     * @param mixed  $default Value returned when the header is absent.
     * @return mixed
     */
    public function header(string $name, $default = null)
    {
        $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
        return $this->server[$key] ?? $default;
    }

    /**
     * Return the client IP address.
     * @return string  e.g. '127.0.0.1'
     */
    public function getIp(): string
    {
        return (string) ($this->server['REMOTE_ADDR'] ?? '0.0.0.0');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // NAMED ROUTE PARAMS (populated by Router)
    // ─────────────────────────────────────────────────────────────────────────

    /**
     * Inject named route parameters after the Router matches a pattern.
     * Called exclusively by Router::dispatch().
     *
     * @param array<string, string> $params e.g. ['id' => '42', 'slug' => 'smith-wedding']
     * @return void
     */
    public function setParams(array $params): void
    {
        $this->routeParams = $params;
    }

    /**
     * Return a named route parameter extracted from the URI pattern.
     * e.g. for route '/app/couple/{id}' and URI '/app/couple/42': param('id') = '42'
     *
     * @param string $key     Parameter name as defined in the route pattern.
     * @param mixed  $default Value returned when the parameter is absent.
     * @return mixed
     */
    public function param(string $key, $default = null)
    {
        return $this->routeParams[$key] ?? $default;
    }

    /**
     * Return all named route parameters as an associative array.
     * @return array<string, string>
     */
    public function allParams(): array
    {
        return $this->routeParams;
    }
}
