<?php

/**
 * Aphiria
 *
 * @link      https://www.aphiria.com
 * @copyright Copyright (C) 2025 David Young
 * @license   https://github.com/aphiria/aphiria/blob/1.x/LICENSE.md
 */

declare(strict_types=1);

namespace Aphiria\Framework\Routing\Components;

use Aphiria\Api\ApiGateway;
use Aphiria\Api\Router;
use Aphiria\Application\IComponent;
use Aphiria\DependencyInjection\IContainer;
use Aphiria\DependencyInjection\ResolutionException;
use Aphiria\DependencyInjection\TargetedContext;
use Aphiria\Net\Http\IRequestHandler;
use Aphiria\Routing\Attributes\AttributeRouteRegistrant;
use Aphiria\Routing\IRouteRegistrant;
use Aphiria\Routing\RouteCollection;
use Aphiria\Routing\RouteCollectionBuilder;
use Aphiria\Routing\RouteCollectionBuilderRouteRegistrant;
use Aphiria\Routing\RouteRegistrantCollection;
use Aphiria\Routing\UriTemplates\Constraints\IRouteVariableConstraint;
use Aphiria\Routing\UriTemplates\Constraints\RouteVariableConstraintFactory;
use Closure;
use RuntimeException;

/**
 * Defines the router component
 */
class RouterComponent implements IComponent
{
    /** @var bool Whether or not attributes are enabled */
    private bool $attributesEnabled = false;
    /** @var list<Closure(RouteCollectionBuilder): void> The list of callbacks that can register route builders */
    private array $callbacks = [];
    /** @var array<string, Closure(mixed...): IRouteVariableConstraint> The mapping of route variable constraint slugs to factories */
    private array $routeVariableConstraintSlugsToFactories = [];

    /**
     * @param IContainer $container The DI container
     */
    public function __construct(private readonly IContainer $container) {}

    /**
     * @inheritdoc
     * @throws ResolutionException Thrown if any dependencies could not be resolved
     */
    public function build(): void
    {
        $routeRegistrants = $this->container->resolve(RouteRegistrantCollection::class);

        if ($this->attributesEnabled) {
            /** @var IRouteRegistrant $attributeRouteRegistrant */
            $attributeRouteRegistrant = null;

            if (!$this->container->tryResolve(AttributeRouteRegistrant::class, $attributeRouteRegistrant)) {
                throw new RuntimeException(AttributeRouteRegistrant::class . ' cannot be null if using attributes');
            }

            $routeRegistrants->add($attributeRouteRegistrant);
        }

        $routeRegistrants->add(new RouteCollectionBuilderRouteRegistrant($this->callbacks));
        $routeRegistrants->registerRoutes($this->container->resolve(RouteCollection::class));
        $this->container->for(
            new TargetedContext(ApiGateway::class),
            fn(IContainer $container) => $container->bindFactory(IRequestHandler::class, fn(): IRequestHandler => $this->container->resolve(Router::class)),
        );

        $routeVariableConstraintFactory = $this->container->resolve(RouteVariableConstraintFactory::class);

        foreach ($this->routeVariableConstraintSlugsToFactories as $slug => $factory) {
            $routeVariableConstraintFactory->registerConstraintFactory($slug, $factory);
        }
    }

    /**
     * Enables route attributes
     *
     * @return static For chaining
     */
    public function withAttributes(): static
    {
        $this->attributesEnabled = true;

        return $this;
    }

    /**
     * Adds routes to the registry
     *
     * @param Closure(RouteCollectionBuilder): void $callback The callback that takes in an instance of RouteCollectionBuilder
     * @return static For chaining
     */
    public function withRoutes(Closure $callback): static
    {
        $this->callbacks[] = $callback;

        return $this;
    }

    /**
     * Registers a custom route variable constraint
     *
     * @param string $slug The slug to register for the route variable constraint
     * @param Closure(mixed...): IRouteVariableConstraint $factory The factory that can optionally take in parameters and create a route constraint from
     * @return static For chaining
     */
    public function withRouteVariableConstraint(string $slug, Closure $factory): static
    {
        $this->routeVariableConstraintSlugsToFactories[$slug] = $factory;

        return $this;
    }
}
