<?php
declare(strict_types=1);

namespace App\Core;

use InvalidArgumentException;

class Router
{
    private array $routes = [];
    private mixed $fallback = null;

    public function __construct(private array $config)
    {
    }

    public function get(string $area, string $path, mixed $handler): void
    {
        $this->add('GET', $area, $path, $handler);
    }

    public function post(string $area, string $path, mixed $handler): void
    {
        $this->add('POST', $area, $path, $handler);
    }

    public function any(string $area, string $path, mixed $handler): void
    {
        $this->add('ANY', $area, $path, $handler);
    }

    public function fallback(mixed $handler): void
    {
        $this->fallback = $handler;
    }

    private function add(string $method, string $area, string $path, mixed $handler): void
    {
        $this->routes[] = [
            'method' => $method,
            'area' => $area,
            'path' => $this->normalizeRoutePath($path),
            'handler' => $handler,
        ];
    }

    public function dispatch(string $method, string $uri, string $area): mixed
    {
        $method = strtoupper($method);
        $path = DomainRouter::routePath($uri, $area, $this->config);

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

            if ($route['method'] !== $method && $route['method'] !== 'ANY') {
                continue;
            }

            $params = $this->match($route['path'], $path);

            if ($params !== false) {
                return $this->call($route['handler'], $params);
            }
        }

        if ($this->fallback === null) {
            throw new InvalidArgumentException('Router fallback is not defined.');
        }

        return $this->call($this->fallback, []);
    }

    private function match(string $route, string $path): array|false
    {
        $route = $this->normalizeRoutePath($route);
        $path = $this->normalizeRoutePath($path);

        if ($route === '/') {
            $pattern = '~^/$~';
        } else {
            $segments = array_map(static function (string $segment): string {
                if (preg_match('~^\{([a-zA-Z_][a-zA-Z0-9_]*)\}$~', $segment, $match)) {
                    return '(?P<' . $match[1] . '>[^/]+)';
                }

                return preg_quote($segment, '~');
            }, explode('/', trim($route, '/')));

            $pattern = '~^/' . implode('/', $segments) . '$~';
        }

        if (!preg_match($pattern, $path, $matches)) {
            return false;
        }

        return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
    }

    private function call(mixed $handler, array $params): mixed
    {
        if (is_callable($handler)) {
            return $handler(...array_values($params));
        }

        if (!is_array($handler) || count($handler) !== 2) {
            throw new InvalidArgumentException('Route handler must be a callable or [class, method].');
        }

        [$class, $method] = $handler;
        $controller = new $class($this->config);

        return $controller->{$method}(...array_values($params));
    }

    private function normalizeRoutePath(string $path): string
    {
        $path = '/' . trim($path, '/');

        return $path === '/' ? '/' : rtrim($path, '/');
    }
}
