<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\CmsBundle\EventListener\Menu;

use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Knp\Menu\ItemInterface;
use Knp\Menu\Util\MenuManipulator;
use Richardhj\ContaoKnpMenuBundle\Event\FrontendMenuEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

/**
 * Injects the public landing page (/o/{alias}) of every edition that opted
 * in via the "CMS integration" admin section into the frontend main
 * navigation, positioned right after the admin-chosen Contao page.
 *
 * Mirrors {@see BackendAdminListener}'s use of the KnpMenu API
 * (factory → createItem, tree->addChild, MenuManipulator->moveToPosition);
 * the dormant {@see FrontendProtectedListener} confirms FrontendMenuEvent's
 * getTree() shape.
 *
 * Two integration points to confirm against the running app + the installed
 * richardhj/contao-knp-menu bundle (both fail safe — wrong guess just
 * appends the item at the end of every page-based frontend menu):
 *
 *  1. MAIN_NAV_NAME — the tree name of the main navigation module. The
 *     bundle dispatches one FrontendMenuEvent per rendered nav module; we
 *     only want to touch the main one. Until confirmed it is left null,
 *     which means "act on any tree" (deduped per tree). Set it to the
 *     module's name once known to scope precisely.
 *  2. childForPage() — how the bundle keys page items. Contao page items
 *     are conventionally named by their page id; we also probe a couple of
 *     common extras. If no match is found the item is appended at the end.
 */
#[AsEventListener]
final class EditionNavigationListener
{
    /**
     * Tree name of the main navigation module, or null to act on every
     * frontend menu tree. See class docblock #1.
     */
    private const ?string MAIN_NAV_NAME = null;

    public function __construct(
        private readonly EditionRepository $editions,
        private readonly UrlGeneratorInterface $urlGenerator,
    ) {
    }

    public function __invoke(FrontendMenuEvent $event): void
    {
        $tree = $event->getTree();

        if (null !== self::MAIN_NAV_NAME && self::MAIN_NAV_NAME !== $tree->getName()) {
            return;
        }

        $editions = $this->editions->findBy(['addToNavigation' => true]);
        if ([] === $editions) {
            return;
        }

        $factory = $event->getFactory();

        foreach ($editions as $edition) {
            if (!$edition instanceof Edition || null === $edition->getAlias()) {
                continue;
            }

            $itemName = 'edition_'.$edition->getId();

            // Idempotency: the same event can fire more than once per request
            // (e.g. desktop + mobile renders); never add the item twice.
            if (null !== $tree->getChild($itemName)) {
                continue;
            }

            $item = $factory->createItem($itemName)
                ->setLabel((string) $edition->getName())
                ->setUri($this->urlGenerator->generate('cms_edition_landing', ['alias' => $edition->getAlias()]))
            ;

            $tree->addChild($item);

            $this->positionAfter($tree, $item, $edition->getNavigationPositionAfter());
        }
    }

    /**
     * Move $item to sit directly after the page identified by $pageId.
     * No-op (item stays appended at the end) when $pageId is null or the
     * target page isn't a child of this tree.
     */
    private function positionAfter(ItemInterface $tree, ItemInterface $item, ?int $pageId): void
    {
        if (null === $pageId) {
            return;
        }

        $target = $this->childForPage($tree, $pageId);
        if (null === $target) {
            return;
        }

        // Children are an ordered map; find the target's 0-based index and
        // drop our item into the slot right after it.
        $index = 0;
        foreach ($tree->getChildren() as $name => $child) {
            if ($name === $target->getName()) {
                break;
            }
            ++$index;
        }

        new MenuManipulator()->moveToPosition($item, $index + 1);
    }

    /**
     * Resolve the tree child that represents Contao page $pageId. See class
     * docblock #2 — keyed by id by convention, with a couple of extra probes.
     */
    private function childForPage(ItemInterface $tree, int $pageId): ?ItemInterface
    {
        if (null !== ($byName = $tree->getChild((string) $pageId))) {
            return $byName;
        }

        foreach ($tree->getChildren() as $child) {
            foreach (['pageModel', 'page', 'id', 'pageId'] as $extra) {
                $value = $child->getExtra($extra);
                if (\is_object($value) && method_exists($value, 'getId') && (int) $value->getId() === $pageId) {
                    return $child;
                }
                if (\is_scalar($value) && (int) $value === $pageId) {
                    return $child;
                }
            }
        }

        return null;
    }
}
