<?php

declare(strict_types=1);

namespace Apexl\Io\Routing;

use Psr\Http\Server\MiddlewareInterface;

/**
 * What was attached to a route, in the order it was attached.
 *
 * Slim keeps middleware only as a chain of anonymous handlers closed over private state,
 * with no way to read it back — so a permission expressed as middleware would be a permission
 * nothing could ask about, and the menu could not hide a page the caller will be refused.
 * IoRoute keeps this list as it goes, because it sees every add().
 *
 * Immutable: `with()` returns a new collection, so nothing holding one can watch it change
 * underneath them.
 */
final readonly class MiddlewareCollection
{
    /** @param list<MiddlewareInterface|string|callable> $middleware */
    public function __construct(private array $middleware = []) {}

    public function with(mixed $middleware): self
    {
        return new self([...$this->middleware, $middleware]);
    }

    /**
     * The first attached middleware of this class, or null.
     *
     * Only instances can be found. `add(SomeMiddleware::class)` takes Slim's deferred path,
     * which stores the name and resolves it from the container at dispatch — there is no
     * instance to hold arguments, so anything a route needs to be asked about must be
     * attached as an object.
     *
     * @template T of MiddlewareInterface
     *
     * @param class-string<T> $middleware
     *
     * @return T|null
     */
    public function find(string $middleware): ?MiddlewareInterface
    {
        foreach ($this->middleware as $candidate) {
            if ($candidate instanceof $middleware) {
                return $candidate;
            }
        }

        return null;
    }

    /**
     * Every attached middleware of this class.
     *
     * A route can be gated more than once — a group that requires an admin, holding a route
     * that requires a particular permission — and all of them have to pass, so anything
     * deciding whether a caller may proceed has to see all of them, not the first.
     *
     * @template T of MiddlewareInterface
     *
     * @param class-string<T> $middleware
     *
     * @return list<T>
     */
    public function findAll(string $middleware): array
    {
        return array_values(array_filter(
            $this->middleware,
            static fn(mixed $candidate): bool => $candidate instanceof $middleware,
        ));
    }

    public function has(string $middleware): bool
    {
        return $this->find($middleware) !== null;
    }

    /** Group middleware first, since that is the order it runs in. */
    public function prepend(self $earlier): self
    {
        return new self([...$earlier->middleware, ...$this->middleware]);
    }

    /** @return list<MiddlewareInterface|string|callable> */
    public function all(): array
    {
        return $this->middleware;
    }
}
