<?php
declare(strict_types=1);

namespace Core;

/**
 * URL → controller dispatch.
 *
 *   Web routes:  /FuelDesk/dashboard           → App\Controllers\Web\FuelDeskController::dashboard()
 *   API routes:  /api/v1/auth/login            → App\Controllers\Api\AuthController::login()
 *
 * URL params after the action become method args:
 *   /api/v1/sites/42/assets  → ApiSitesController::assets(42)
 */
final class Router
{
    public static function dispatch(string $rawPath): void
    {
        $path = trim(parse_url($rawPath, PHP_URL_PATH) ?? '/', '/');

        // Strip optional PROOT prefix if present (when not using public/ as docroot).
        $proot = trim(PROOT, '/');
        if ($proot !== '' && str_starts_with($path, $proot)) {
            $path = ltrim(substr($path, strlen($proot)), '/');
        }

        $segments = $path === '' ? [] : explode('/', $path);

        try {
            if (($segments[0] ?? '') === 'api') {
                self::dispatchApi(array_slice($segments, 1));
            } else {
                self::dispatchWeb($segments);
            }
        } catch (\Throwable $e) {
            self::handleError($e);
        }
    }

    private static function dispatchWeb(array $segments): void
    {
        $controllerName = $segments[0] ?? 'Home';
        $action         = $segments[1] ?? 'index';
        $args           = array_slice($segments, 2);

        $fqcn = "App\\Controllers\\Web\\{$controllerName}Controller";
        self::invoke($fqcn, $action, $args, kind: 'web');
    }

    private static function dispatchApi(array $segments): void
    {
        // Expect /api/v1/<resource>/<action>/<args...>
        $version = $segments[0] ?? '';
        if ($version !== 'v1') {
            json_response(['error' => 'Unsupported API version'], 404);
        }
        $resource = $segments[1] ?? '';
        $action   = $segments[2] ?? 'index';
        $args     = array_slice($segments, 3);

        if ($resource === '') {
            json_response(['error' => 'Missing resource'], 404);
        }

        // kebab-case → PascalCase so /api/v1/work-orders reaches WorkOrdersController.
        $className = str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $resource)));
        $fqcn = "App\\Controllers\\Api\\{$className}Controller";
        // Same normalisation for the action segment so /complete-task → completeTask.
        $action = lcfirst(str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $action))));
        self::invoke($fqcn, $action, $args, kind: 'api');
    }

    private static function invoke(string $fqcn, string $action, array $args, string $kind): void
    {
        if (!class_exists($fqcn)) {
            self::notFound($kind, "Controller not found: {$fqcn}");
        }
        $controller = new $fqcn();
        if (!method_exists($controller, $action)) {
            self::notFound($kind, "Action not found: {$fqcn}::{$action}");
        }

        $context = ['kind' => $kind, 'controller' => $fqcn, 'action' => $action];

        foreach ($controller->middleware() as $mw) {
            (new $mw())->handle($context);
        }

        // URL segments arrive as strings; the action may declare typed params
        // (e.g. `int $id`). Coerce each arg to match its declared type so
        // controllers can keep strict `int $id` signatures without every route
        // having to hand-cast. Silently skips coercion when a type is unclear.
        $args = self::coerceArgs($controller, $action, $args);

        $controller->{$action}(...$args);
    }

    private static function coerceArgs(object $controller, string $action, array $args): array
    {
        try {
            $reflection = new \ReflectionMethod($controller, $action);
        } catch (\Throwable) {
            return $args;
        }
        $params = $reflection->getParameters();
        foreach ($args as $i => $v) {
            if (!isset($params[$i])) break;
            $type = $params[$i]->getType();
            if (!$type instanceof \ReflectionNamedType) continue;
            switch ($type->getName()) {
                case 'int':   $args[$i] = is_numeric($v) ? (int)   $v : $v; break;
                case 'float': $args[$i] = is_numeric($v) ? (float) $v : $v; break;
                case 'bool':  $args[$i] = filter_var($v, FILTER_VALIDATE_BOOLEAN); break;
                default: /* leave as string */
            }
        }
        return $args;
    }

    private static function notFound(string $kind, string $msg): void
    {
        if ($kind === 'api') {
            json_response(['error' => 'Not found', 'detail' => APP_DEBUG ? $msg : null], 404);
        }
        http_response_code(404);
        echo APP_DEBUG ? "<h1>404 — {$msg}</h1>" : '<h1>404 Not Found</h1>';
        exit;
    }

    private static function handleError(\Throwable $e): void
    {
        error_log('[router] ' . $e->getMessage() . "\n" . $e->getTraceAsString());
        $isApi = str_starts_with($_SERVER['PATH_INFO'] ?? $_SERVER['REQUEST_URI'] ?? '', '/api');

        if ($isApi || str_contains((string)($_SERVER['HTTP_ACCEPT'] ?? ''), 'application/json')) {
            json_response([
                'error'  => 'Server error',
                'detail' => APP_DEBUG ? $e->getMessage() : null,
                'trace'  => APP_DEBUG ? explode("\n", $e->getTraceAsString()) : null,
            ], 500);
        }

        http_response_code(500);
        if (APP_DEBUG) {
            echo "<h1>Server error</h1><pre>" . e($e->getMessage()) . "\n\n" . e($e->getTraceAsString()) . "</pre>";
        } else {
            echo "<h1>Something went wrong.</h1>";
        }
        exit;
    }
}
