<?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\AdminBundle\Controller\Fragment;

use Ferienpass\AdminBundle\Controller\TagsResponses;
use Ferienpass\CoreBundle\HttpCache\CacheTag;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Response;

/**
 * ESI fragment that renders the header badge counting attendances currently
 * sitting in a stash (staged, awaiting commit on /admin/anmeldungen-festschreiben).
 *
 * The surrounding admin page is cached for a year (see fos_http_cache.yaml), so
 * the badge would otherwise freeze at whatever count was rendered when the page
 * was first cached. Rendering it as an ESI fragment lets Varnish cache the badge
 * in its own entry with a short TTL while the outer page stays cached.
 *
 * Invalidation is tag-based (xkey), like UserNavigationController — but the count
 * is a tenant-global aggregate with no entity UUID, so it carries the synthetic
 * {@see CacheTag::stash()} tag (`stash.<app.id>`) instead of an entity tag. The
 * CacheInvalidationListener purges it whenever an attendance's stash membership
 * changes, and the CommitAttendances worker purges it on commit (bulk DQL, no
 * Doctrine events). Every purge reaches Varnish promptly — request paths flush at
 * kernel.terminate, worker paths via FlushCacheManagerOnWorkerMessageListener — so
 * the s-maxage can be the full year like every other cacheable route; the tag,
 * not the TTL, keeps it fresh.
 *
 * On the shared build the tag is not tenant-unique (app.id is '' there), so a
 * purge harmlessly over-invalidates every tenant's badge — see {@see CacheTag::stash()}.
 *
 * Not a mapped route: dispatched at request time by Symfony's FragmentListener
 * (framework.fragments.path: /_fragment) via render_esi() in _header.html.twig.
 */
final class StashCountController extends AbstractController
{
    use TagsResponses;

    private const int TTL = 31556926;

    public function __invoke(AttendanceRepository $attendances, #[Autowire(param: 'app.id')] string $appId): Response
    {
        // Defence in depth: the ESI include only sits inside the ROLE_ADMIN
        // block of _header.html.twig, but the signed fragment URI is otherwise
        // reachable on its own — keep the badge admin-only.
        if (!$this->isGranted('ROLE_ADMIN')) {
            return new Response('');
        }

        $response = $this->render('@FerienpassAdmin/layout/_stash_count.html.twig', [
            'count' => $attendances->countStashed(),
        ]);

        // Tenant-global and identical for every admin of the tenant, so it can
        // be shared across sessions — no Vary: Cookie needed. The count itself
        // is tenant-scoped because Varnish keys the fragment by host.
        $response->setPublic();
        $response->setSharedMaxAge(self::TTL);
        $this->tagResponse(CacheTag::stash($appId));

        return $response;
    }
}
