<?php declare(strict_types=1);
/**
 * @package messenger
 * @author kputyr
 * @date 20.04.2026
 */

namespace IMATHUZH\Messenger\Rest;

/**
 * A class representing an HTTP request to the REST API. It provides methods
 * to access the basic properties of a request (path, method, query parameters
 * and body), as well as to extract parameters from the path.
 */
class RestRequest implements ServerRequestInterface
{
    protected array $params;
    protected string $path;

    public function __construct(
        string $path = '/',
        protected string $method = 'GET',
        protected ?array $query = null,
        protected ?object $body = null
    ) {
        $this->path = preg_replace('/\/+/', '/', "/$path/");
    }

    /**
     * Creates a request object from global variables. The path is taken
     * from the `X-Original-URI` header, which is set by the web server
     * and contains the original request URI. The method is taken from
     * the `REQUEST_METHOD` server variable. The query parameters are
     * taken from the `$_GET` superglobal and the body is parsed as JSON
     * from the input stream.
     * 
     * @return RestRequest
     */
    public static function fromGlobals(): self
    {
        return new self(
            $_SERVER['HTTP_X_ORIGINAL_URI'] ?? '',
            $_SERVER['REQUEST_METHOD'] ?? 'GET',
            $_GET ?? null,
            json_decode(file_get_contents('php://input')) ?: null
        );
    }

    /**
     * Injects parameters into the request object. This is used to pass
     * parameters extracted from the request path to the request handlers.
     *
     * @param array $params
     * @return void
     */
    public function injectParams(array $params): void
    {
        $this->params = $params;
    }

    /**
     * @inheritDoc
     */
    public function getPath(): string
    {
        return $this->path;
    }

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

    /**
     * @inheritDoc
     */
    public function getBody(array $patterns = []): null|object|array
    {
        return $patterns ? $this->checkValues($patterns, (array) $this->body) : $this->body;
    }

    /**
     * @inheritDoc
     */
    public function getQuery(array $patterns = []): array
    {
        return $this->checkValues($patterns, $this->query);
    }

    /**
     * @inheritDoc
     */
    public function getParams(string ...$patterns): array
    {
        return $this->checkValues($patterns, $this->params);
    }

    /**
     * Checks that the given values match the provided patterns and rejects
     * the request if not. The patterns are matched against the values in
     * the order they are provided or by keys when both parameters are
     * associated arrays.
     * 
     * If no patterns are provided, the values are returned as is.     
     * 
     * @param array $patterns
     * @param array $values
     * @return array
     */
    protected function checkValues(array $patterns, array $values): array
    {
        if (!$patterns) {
            return $values;
        }
        $checked = [];
        foreach ($patterns as $key => $pattern) {
            $value = $values[$key] ?? null;
            if ($value === null) {
                if (\is_int($key)) {
                    $this->reject("not enough arguments");
                } else {
                    $this->reject("missing parameters: '$key'");
                }
            } elseif (!preg_match($pattern, $value)) {
                $this->reject("invalid value: '$value'");
            } else {
                $checked[$key] = $value;
            }
        }
        return $checked;
    }

    /**
     * @inheritDoc
     */
    public function assertMatches(string $value, string $pattern): void
    {
        if (!preg_match($pattern, $value)) {
            $this->reject("invalid value: '$value'");
        }
    }

    /**
     * @inheritDoc
     */
    public function reject(string $message, int $code=403): void
    {
        throw new \RuntimeException($message, $code);
    }
}