<?php

declare(strict_types=1);

namespace App\Core;

use Closure;
use RuntimeException;

final class Router
{
    /** @var array<int, array{method:string,path:string,handler:callable,middleware:array<int,callable>,name:?string}> */
    private array $routes = [];

    public function get(string $path, callable|array $handler, array $middleware = [], ?string $name = null): void
    {
        $this->add('GET', $path, $handler, $middleware, $name);
    }

    public function post(string $path, callable|array $handler, array $middleware = [], ?string $name = null): void
    {
        $this->add('POST', $path, $handler, $middleware, $name);
    }

    private function add(string $method, string $path, callable|array $handler, array $middleware, ?string $name): void
    {
        $this->routes[] = [
            'method' => strtoupper($method),
            'path' => '/' . trim($path, '/'),
            'handler' => $handler,
            'middleware' => $middleware,
            'name' => $name,
        ];
    }

    /** @return array{controller:Closure,params:array<string,string>} */
    public function dispatch(Request $request): array
    {
        foreach ($this->routes as $route) {
            if ($route['method'] !== $request->method()) {
                continue;
            }

            $pattern = preg_replace_callback(
                '/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/',
                static fn(array $m): string => '(?P<' . $m[1] . '>[^/]+)',
                $route['path']
            );

            $pattern = '#^' . $pattern . '/?$#';

            if (preg_match($pattern, $request->path(), $matches) !== 1) {
                continue;
            }

            $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
            $handler = $route['handler'];
            $middleware = $route['middleware'];

            $controller = function () use ($handler, $params, $middleware, $request): mixed {
                $destination = function () use ($handler, $params): mixed {
                    return $this->invokeHandler($handler, $params);
                };

                foreach (array_reverse($middleware) as $layer) {
                    $next = $destination;
                    $destination = static function () use ($layer, $request, $next): mixed {
                        if ($layer instanceof Middleware) {
                            return $layer->handle($request, $next);
                        }

                        return $layer($request, $next);
                    };
                }

                return $destination();
            };

            return ['controller' => $controller, 'params' => $params];
        }

        throw new RuntimeException('Route not found', 404);
    }

    private function invokeHandler(callable|array $handler, array $params): mixed
    {
        if (is_array($handler)) {
            [$class, $method] = $handler;
            $instance = new $class();
            $arguments = $this->coerceRouteParameters([$class, $method], $params);
            return $instance->{$method}(...$arguments);
        }

        return $handler(...array_values($params));
    }

    /** @return array<int,mixed> */
    private function coerceRouteParameters(array $handler, array $params): array
    {
        $reflection = new \ReflectionMethod($handler[0], $handler[1]);
        $values = array_values($params);

        foreach ($reflection->getParameters() as $index => $parameter) {
            if (!array_key_exists($index, $values)) {
                continue;
            }

            $type = $parameter->getType();
            if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
                continue;
            }

            if ($type instanceof \ReflectionNamedType && $type->getName() === 'int') {
                if (!preg_match('/^-?\d+$/', (string) $values[$index])) {
                    throw new RuntimeException('Invalid route parameter.', 400);
                }
                $values[$index] = (int) $values[$index];
            } elseif ($type instanceof \ReflectionNamedType && $type->getName() === 'float') {
                if (!is_numeric($values[$index])) {
                    throw new RuntimeException('Invalid route parameter.', 400);
                }
                $values[$index] = (float) $values[$index];
            }
        }

        return $values;
    }
}
