<?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\CoreBundle\EventListener\HttpCache;

use Contao\CoreBundle\EventListener\SubrequestCacheSubscriber;
use Contao\CoreBundle\Routing\ScopeMatcher;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;

/**
 * Opt every Contao main response (frontend AND backend) into Contao's
 * sub-request cache-merge flow by stamping
 * `Contao-Merge-Cache-Control: 1` on it — see
 * {@see SubrequestCacheSubscriber}.
 *
 * Without this, a fragment that declares itself `Cache-Control: private`
 * (e.g. our `AbstractPrivateFragmentController`-based account and
 * application fragments) gets captured into the subscriber's per-request
 * strategy, but `update()` is never called on the main response because
 * only the subscriber's OWN signal (`Contao-Merge-Cache-Control: 1` on
 * the main response) triggers it. Contao's stock `AbstractPageController`
 * stamps this header; Ferienpass CMS pages extend a different base
 * (`Ferienpass\CmsBundle\Controller\AbstractController`) and build
 * their response via a custom `PageBuilder`, so neither path stamps the
 * header automatically. Doing it centrally here — one listener covers
 * every page regardless of how it was built — prevents the
 * `/meine-anmeldungen` class of leak where the page stays `public` and
 * Varnish serves user A's cached applications list to user B.
 *
 * ### Why it's safe to stamp unconditionally
 *
 * The subscriber's `update()` is idempotent on pages without embedded
 * fragments (`embeddedResponses === 0` short-circuits early) — the stamp
 * is a no-op for fragment-less pages. For pages with fragments, the
 * merge applies the strictest Cache-Control from any child, which is
 * exactly the intended behaviour.
 *
 * ### Priority
 *
 * Must run BEFORE the subscriber's `onKernelResponse` (priority -255) so
 * the header is present when the subscriber inspects it. Any positive
 * priority works; kept at 0 for predictability.
 */
final class EnableCacheMergeListener
{
    public function __construct(
        private readonly ScopeMatcher $scopeMatcher,
    ) {
    }

    #[AsEventListener(priority: 0)]
    public function __invoke(ResponseEvent $event): void
    {
        if (!$this->scopeMatcher->isContaoMainRequest($event)) {
            return;
        }

        $event->getResponse()->headers->set(SubrequestCacheSubscriber::MERGE_CACHE_HEADER, '1');
    }
}
