<?php
declare(strict_types=1);

namespace App\Core\Routing;

use App\Core\Http\Request;
use App\Core\Http\Response;
use RuntimeException;

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

    /**
     * @param callable $handler
     * @param array<int, callable|object> $middlewares
     */
    public function get(string $path, callable $handler, array $middlewares = []): void
    {
        $this->add('GET', $path, $handler, $middlewares);
    }

    /**
     * @param callable $handler
     * @param array<int, callable|object> $middlewares
     */
    public function post(string $path, callable $handler, array $middlewares = []): void
    {
        $this->add('POST', $path, $handler, $middlewares);
    }

    public function dispatch(Request $request): Response
    {
        $route = $this->find($request->method(), $request->path());
        if ($route === null) {
            return Response::html('Not found', 404);
        }

        $params = (array) ($route['params'] ?? []);
        if ($params !== []) {
            $request = $request->withAttributes($params);
        }

        $handler = $route['handler'];
        $middlewares = $route['middlewares'];

        $pipeline = array_reduce(
            array_reverse($middlewares),
            fn (callable $next, callable|object $middleware): callable => $this->wrapMiddleware($middleware, $next),
            fn (Request $req): mixed => $handler($req)
        );

        $result = $pipeline($request);

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

        if (is_string($result)) {
            return Response::html($result);
        }

        if (is_array($result)) {
            return Response::json($result);
        }

        return Response::html('');
    }

    /**
     * @param callable $handler
     * @param array<int, callable|object> $middlewares
     */
    private function add(string $method, string $path, callable $handler, array $middlewares): void
    {
        $this->routes[] = [
            'method' => strtoupper($method),
            'path' => $this->normalizePath($path),
            'handler' => $handler,
            'middlewares' => $middlewares,
        ];
    }

    /**
     * @return array{method:string,path:string,handler:callable,middlewares:array<int, callable|object>}|null
     */
    private function find(string $method, string $path): ?array
    {
        $normalizedMethod = strtoupper($method);
        $normalizedPath = $this->normalizePath($path);

        foreach ($this->routes as $route) {
            if ($route['method'] === $normalizedMethod && $route['path'] === $normalizedPath) {
                return $route;
            }
        }

        foreach ($this->routes as $route) {
            if ($route['method'] !== $normalizedMethod) {
                continue;
            }
            if (!$this->hasRouteParams($route['path'])) {
                continue;
            }

            $match = $this->matchPathParams($route['path'], $normalizedPath);
            if ($match === null) {
                continue;
            }

            $route['params'] = $match;
            return $route;
        }

        return null;
    }

    private function normalizePath(string $path): string
    {
        if ($path === '') {
            return '/';
        }

        if ($path !== '/' && str_ends_with($path, '/')) {
            return rtrim($path, '/');
        }

        return $path;
    }

    private function wrapMiddleware(callable|object $middleware, callable $next): callable
    {
        return function (Request $request) use ($middleware, $next): mixed {
            if (is_callable($middleware)) {
                return $middleware($request, $next);
            }

            if (method_exists($middleware, 'handle')) {
                return $middleware->handle($request, $next);
            }

            throw new RuntimeException('Invalid middleware definition');
        };
    }

    private function hasRouteParams(string $path): bool
    {
        return str_contains($path, '{') && str_contains($path, '}');
    }

    /**
     * @return array<string, string>|null
     */
    private function matchPathParams(string $routePath, string $requestPath): ?array
    {
        $routeSegments = explode('/', trim($routePath, '/'));
        $regexSegments = [];
        foreach ($routeSegments as $segment) {
            if (preg_match('/^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$/', $segment, $match) === 1) {
                $regexSegments[] = '(?P<' . $match[1] . '>[^/]+)';
                continue;
            }

            $regexSegments[] = preg_quote($segment, '#');
        }

        $pattern = '#^/' . implode('/', $regexSegments) . '$#';
        if ($routePath === '/') {
            $pattern = '#^/$#';
        }
        if (preg_match($pattern, $requestPath, $matches) !== 1) {
            return null;
        }

        $params = [];
        foreach ($matches as $key => $value) {
            if (is_string($key)) {
                $params[$key] = (string) $value;
            }
        }

        return $params;
    }
}
