<?php

declare(strict_types=1);

namespace Apexl\Io\Routing;

use Psr\Http\Server\MiddlewareInterface;
use RuntimeException;
use Slim\Interfaces\RouteCollectorInterface;
use Slim\Interfaces\RouteInterface;
use Slim\Routing\Route;

/**
 * A Slim route that remembers what was attached to it, and which module registered it.
 *
 * Everything a route needs to say about itself now says it here rather than through
 * `setArgument()`, which Slim deprecated in 4.14.1, which holds strings only — the reason
 * permissions used to be serialized and unserialized on every request — and which is merged
 * into `getArguments()`, where io-validation spreads it into the data a request is validated
 * against.
 *
 * Recording middleware as it arrives is the alternative to reflecting into Slim's private
 * dispatcher. That walk does work, but it reads implementation detail across two levels of
 * anonymous classes, and when it stops working it returns null rather than raising — which
 * reads exactly like "this route is unrestricted".
 */
final class IoRoute extends Route
{
    private MiddlewareCollection $middlewareCollection;

    private ?string $module = null;

    private ?RouteCollectorInterface $collector = null;

    /**
     * The collector that built this, so naming can replace an earlier route of the same name.
     *
     * Slim's Route has no idea which collector holds it, and the answer is only knowable at
     * construction, so it is handed over there rather than looked up.
     */
    public function collectedBy(RouteCollectorInterface $collector): self
    {
        $this->collector = $collector;

        return $this;
    }

    /**
     * Naming a route takes the name from whatever held it before.
     *
     * Slim resolves a name to the first route carrying it and keeps both, so a module
     * re-registering a name would leave the older route answering and the new one
     * unreachable — the exact opposite of what overriding a route means. Doing this on
     * setName() rather than at registration is what makes it work for a route named the
     * ordinary way, chained after the verb.
     */
    public function setName(string $name): RouteInterface
    {
        $this->takeNameFromAnyOlderRoute($name);

        return parent::setName($name);
    }

    private function takeNameFromAnyOlderRoute(string $name): void
    {
        if ($this->collector === null) {
            return;
        }

        try {
            $older = $this->collector->getNamedRoute($name);
        } catch (RuntimeException) {
            return;
        }

        if ($older !== $this) {
            $this->collector->removeNamedRoute($name);
        }
    }

    /**
     * Everything gating this route: what its groups carry, then its own.
     *
     * Groups are read on each call rather than copied in when the route is built, because
     * `$app->group(...)->add(...)` attaches to the group *after* the closure has registered
     * the routes inside it. The route holds the group object, so asking it late is the only
     * way to see that middleware at all.
     */
    public function getMiddleware(): MiddlewareCollection
    {
        $own = $this->middlewareCollection ??= new MiddlewareCollection();

        // Outermost group first, which is the order they run in: Slim reverses this list
        // when it builds the stack, so reading it forwards would report a nested group as
        // gating before the group that contains it.
        foreach (array_reverse($this->getGroups()) as $group) {
            if ($group instanceof IoRouteGroup) {
                $own = $own->prepend($group->getMiddleware());
            }
        }

        return $own;
    }

    /**
     * @param MiddlewareInterface|string|callable $middleware
     */
    public function add($middleware): RouteInterface
    {
        $this->middlewareCollection = $this->getMiddleware()->with($middleware);

        return parent::add($middleware);
    }

    public function addMiddleware(MiddlewareInterface $middleware): RouteInterface
    {
        $this->middlewareCollection = $this->getMiddleware()->with($middleware);

        return parent::addMiddleware($middleware);
    }

    /** Which module registered this, for grouping in `routes:dump`. */
    public function setModule(string $module): self
    {
        $this->module = $module;

        return $this;
    }

    public function getModule(): ?string
    {
        return $this->module;
    }
}
