<?php

declare(strict_types=1);

namespace Apexl\Io\System\Services;

use Apexl\Io\Routing\DeclaresRoutePermission;
use Apexl\Io\Routing\IoRoute;
use Slim\App;
use Slim\Interfaces\RouteInterface;

/**
 * The registered routes, grouped by the module that registered them, for `routes:dump` and
 * the debug endpoint behind it.
 *
 * Read from Slim, which is the only place routes live. It used to read a parallel table
 * RouteManager kept for the purpose — the last thing that did.
 */
final readonly class RouteDebugHelper
{
    public function __construct(private App $application) {}

    public function routes(): array
    {
        $routes = [];

        foreach ($this->application->getRouteCollector()->getRoutes() as $route) {
            $module = $route instanceof IoRoute ? $route->getModule() : null;

            $routes[$module ?? 'Core'][] = [
                'verb' => implode('|', $route->getMethods()),
                'pattern' => $route->getPattern(),
                'name' => $route->getName(),
                'callable' => $this->describeCallable($route),
                'args' => array_filter([
                    'permission' => $this->describePermissions($route),
                ]),
            ];
        }

        return $routes;
    }

    /**
     * Every gate on the route — its groups' and its own — in the order they run.
     *
     * Permissions are the middleware that enforces them rather than serialized strings, so
     * the dump prints what they are called instead of `E:59:"...:READ__ALL";`.
     */
    private function describePermissions(RouteInterface $route): ?string
    {
        if (!$route instanceof IoRoute) {
            return null;
        }

        $described = array_map(
            fn(DeclaresRoutePermission $gate): string => (string) $this->describePermission($gate->getPermission()),
            $route->getMiddleware()->findAll(DeclaresRoutePermission::class),
        );

        return $described === [] ? null : implode(' + ', $described);
    }

    private function describePermission(object|array|null $permission): ?string
    {
        if ($permission === null) {
            return null;
        }

        $names = array_map(
            static function (object $case): string {
                if (!isset($case->name)) {
                    return $case::class;
                }

                // Short class name plus case: `READ__ALL` on its own does not say which of
                // the forty-odd permission enums it belongs to.
                $enum = strrchr($case::class, '\\');

                return sprintf('%s::%s', $enum === false ? $case::class : substr($enum, 1), $case->name);
            },
            is_array($permission) ? $permission : [$permission],
        );

        return implode(', ', $names);
    }

    /**
     * A callable is a class-string, an array pair or a closure; the last has no name worth
     * printing, so it is described by where it was written.
     */
    private function describeCallable(RouteInterface $route): string
    {
        $callable = $route->getCallable();

        if (is_string($callable)) {
            return $callable;
        }

        if (is_array($callable)) {
            return sprintf('%s::%s', is_object($callable[0]) ? $callable[0]::class : $callable[0], $callable[1] ?? '__invoke');
        }

        return $this->describeClosure($callable);
    }

    private function describeClosure(callable $callable): string
    {
        $reflection = new \ReflectionFunction($callable(...));

        return sprintf(
            'closure:%s:%d',
            basename((string) $reflection->getFileName()),
            $reflection->getStartLine(),
        );
    }
}
