<?php

declare(strict_types=1);

namespace Apexl\Io\Tabulator;

use Apexl\Config\ConfigurationInterface;
use Apexl\Io\Exceptions\RouteNotFoundException;
use Apexl\Io\HookManager;
use Apexl\Io\Menu\Dtos\MenuMetadata;
use Apexl\Io\Menu\Interfaces\HasMenuEntryInterface;
use Apexl\Io\Module;
use Apexl\Io\System\Interfaces\ProvidesRoutesInterface;
use Apexl\Io\Tabulator\Collections\HasTabulatorViewInterfaceClassCollection;
use Apexl\Io\Tabulator\Collections\MenuMetadataCache;
use Apexl\Io\Tabulator\Factories\TabulatorAjaxControllerFactory;
use Apexl\Io\Tabulator\Factories\TabulatorControllerFactory;
use Apexl\Io\Tabulator\Factories\TabulatorDisplayControllerFactory;
use Apexl\Io\Tabulator\Interfaces\HasTabulatorViewInterface;
use Apexl\Io\User\Enums\Permissions\Common;
use Apexl\Io\User\Middleware\RoutePermissionMiddleware;
use Apexl\Utils\Strings\DashCase;
use DI\DependencyException;
use DI\NotFoundException;
use Slim\App;

final readonly class TabulatorModule extends Module implements ProvidesRoutesInterface
{
    public const string HOOK__COLLECT_ENTITIES = 'hook.collect_entities';

    /** Assigned once, by entities(); a readonly property may hold no default. */
    private ?HasTabulatorViewInterfaceClassCollection $entitiesCache;

    private MenuMetadataCache $menuMetadataCache;

    public function __construct(
        ConfigurationInterface $config,
        readonly private HookManager $hookManager,
        readonly private TabulatorControllerFactory $tabulatorControllerFactory,
        readonly private TabulatorAjaxControllerFactory $tabulatorAjaxControllerFactory,
        readonly private TabulatorDisplayControllerFactory $tabulatorDisplayControllerFactory,
        App $app,
    ) {
        parent::__construct($config, $app);

        $this->menuMetadataCache = new MenuMetadataCache();
    }

    /**
     * @throws DependencyException
     * @throws NotFoundException|RouteNotFoundException
     */
    public function routes(): void
    {
        $this->tabulatableRoutes();
        $this->accountRoutes();
    }

    /**
     * @throws DependencyException
     * @throws NotFoundException
     */
    private function tabulatableRoutes(): void
    {
        $this->entities()->each(fn(string $entityClass) => $this->tabulatableRoute(
            $entityClass
        ));
    }

    /**
     * Collected once per request and memoised — the collect hook constructs nothing
     * (it yields class-strings), and both route passes share the same collection.
     */
    private function entities(): HasTabulatorViewInterfaceClassCollection
    {
        return $this->entitiesCache ??= $this->hookManager->processHook(
            self::HOOK__COLLECT_ENTITIES,
            new HasTabulatorViewInterfaceClassCollection()
        );
    }

    /**
     * @param class-string<HasTabulatorViewInterface> $entityClass
     * @throws DependencyException
     * @throws NotFoundException
     */
    private function tabulatableRoute(string $entityClass): void
    {
        $name = DashCase::dashCase($entityClass::shortName());

        // These two routes serve the SAME rows as the list page registered by
        // accountRoute() below, so they must carry the SAME permission. With nothing
        // attached, nothing gates them — the entire table would be readable by an
        // unauthenticated caller. An entity that doesn't opt into the menu surface has no
        // declared permission to inherit, so it falls back to "must be signed in" rather
        // than staying open.
        $permission = is_a($entityClass, HasMenuEntryInterface::class, true)
            ? $this->menuMetadata($entityClass)->permission
            : Common::IS_LOGGED_IN;

        $this->get(sprintf('/tabulator/data/tabulator/%s', $name), $this->tabulatorControllerFactory->make($entityClass))
            ->setName($entityClass::tabulatorRouteName())->add(new RoutePermissionMiddleware($permission));

        $this->get(sprintf('/tabulator/data/tabulator/ajax/%s', $name), $this->tabulatorAjaxControllerFactory->make($entityClass))
            ->setName($entityClass::tabulatorAjaxRouteName())->add(new RoutePermissionMiddleware($permission));
    }

    /**
     * Menu metadata is read twice per entity (data-route permission +
     * account route), and reading it can construct the entity on packages
     * still using the deprecated instance getters — memoise it.
     *
     * @param class-string<HasMenuEntryInterface> $entityClass
     */
    private function menuMetadata(string $entityClass): MenuMetadata
    {
        return $this->menuMetadataCache->for($entityClass);
    }

    /**
     * @throws DependencyException
     * @throws NotFoundException
     * @throws RouteNotFoundException
     */
    private function accountRoutes(): void
    {
        foreach ($this->entities() as $entityClass) {
            // Only entities that opt into the menu surface get an account
            // route — accountRoute reads menu metadata, so it only applies to
            // entities implementing the menu interface. Guard here so
            // downstream modules can register entities without implementing
            // the menu interface.
            if (is_a($entityClass, HasMenuEntryInterface::class, true)) {
                $this->accountRoute($entityClass);
            }
        }
    }

    /**
     * @param class-string<HasMenuEntryInterface> $entityClass
     * @throws DependencyException
     * @throws NotFoundException
     * @throws RouteNotFoundException
     */
    private function accountRoute(string $entityClass): void
    {
        $menu = $this->menuMetadata($entityClass);

        $this->get($menu->pattern, $this->tabulatorDisplayControllerFactory->make($entityClass))
            ->setName($menu->route)->add(new RoutePermissionMiddleware($menu->permission));
    }

}
