final class Router
{
    private array $routes = [];

    public function add(string $method, string $pattern, callable|string $handler): void
    {
        $this->routes[] = [$method, $pattern, $handler];
    }

    public function dispatch(Request $request): Response
    {
        $path = trim($request->path(), '/');
        $method = $request->method();

        foreach ($this->routes as [$routeMethod, $pattern, $handler]) {
            if ($method !== $routeMethod) {
                continue;
            }

            if (preg_match($pattern, $path, $matches)) {
                array_shift($matches);

                return $this->execute($handler, $request, $matches);
            }
        }

        return new Response(404, ['error' => 'Not found']);
    }

    private function execute(callable|string $handler, Request $request, array $params): Response
    {
        try {
            if (is_string($handler)) {
                $handler = new $handler();
            }

            $result = $handler($request, ...$params);

            if ($result instanceof Response) {
                return $result;
            }

            return new Response(200, ['success' => true, 'data' => $result]);

        } catch (\Throwable $e) {
            error_log($e);
            return new Response(500, ['error' => 'Internal server error']);
        }
    }
}

$router = new Router();

// Rentables
$router->add('GET',  '#^rentables$#', Rentables\Index::class);
$router->add('GET',  '#^rentables/(\d+)$#', Rentables\Show::class);

// Pricing
$router->add('GET',  '#^pricing/rates$#', Pricing\Rates\Index::class);
$router->add('POST', '#^pricing/rates$#', Pricing\Rates\Store::class);
$router->add('PUT',  '#^pricing/rates/(\d+)$#', Pricing\Rates\Update::class);

$router->add('GET',  '#^pricing/durations$#', Pricing\Durations\Index::class);
$router->add('POST', '#^pricing/durations$#', Pricing\Durations\Store::class);

$router->add('GET',  '#^pricing/rules$#', Pricing\Rules\Index::class);
$router->add('POST', '#^pricing/rules$#', Pricing\Rules\Store::class);

namespace App\Api\Pricing\Rates;

use App\Http\Request;
use App\Http\Response;

final class Store
{
    public function __invoke(Request $request): Response
    {
        $data = $request->json();

        // validate (later centraliseren)
        if (empty($data['rentable_id'])) {
            return new Response(422, ['error' => 'rentable_id required']);
        }

        // business logic
        // insert pricing_rate rows

        return new Response(201, ['success' => true]);
    }
}

final class Request
{
    public function __construct(
        private string $method,
        private string $path,
        private array $query,
        private array $body,
        private array $headers
    ) {}

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

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

    public function json(): array
    {
        return $this->body;
    }
}

final class Response
{
    public function __construct(
        private int $status,
        private array $body = [],
        private array $headers = []
    ) {}

    public function send(): void
    {
        http_response_code($this->status);

        header('Content-Type: application/json');

        foreach ($this->headers as $k => $v) {
            header("$k: $v");
        }

        echo json_encode($this->body);
    }
}

interface Middleware
{
    public function handle(Request $request, callable $next): Response;
}


final class AuthMiddleware implements Middleware
{
    public function handle(Request $request, callable $next): Response
    {
        if (!$request->input('api_key')) {
            return new Response(401, ['error' => 'Unauthorized']);
        }

        return $next($request);
    }
}~~~~
