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

use Ferienpass\CmsBundle\Controller\AbstractController;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\HttpCache\CacheTag;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Offer-details fragment, rendered as a Varnish-stitched ESI sub-request.
 *
 * Registered with `renderer: 'esi'` in `cms-bundle/config/services.php` so
 * PageBuilder emits `<esi:include src=".../_fragment?...">` for this
 * fragment instead of dispatching it as an in-process sub-request. The
 * fragment is then fetched by Varnish as its OWN top-level HTTP request,
 * which means:
 *
 *   1. Varnish supplies a fresh `Surrogate-Capability` header on the
 *      fragment fetch (set in `varnish/default.vcl:129` on every backend
 *      fetch including ESI sub-fetches).
 *   2. The nested `render_esi(controller(ApplicationFormController))` inside
 *      `offer_details.html.twig` runs in a MAIN-request context, sees that
 *      header, and emits its own `<esi:include>` marker correctly. The
 *      per-user form body NEVER lands in this fragment's response.
 *   3. Varnish caches this fragment per-offer (public, no per-user data
 *      inside), and the application form per-cookie (Vary: Cookie + private).
 *
 * ### Why the signature takes `string $offer` instead of `OfferInterface $offer`
 *
 * Contao's {@see \Contao\CoreBundle\Fragment\FragmentHandler} silently
 * downgrades the renderer to `'forward'` whenever the FragmentReference
 * attributes contain non-scalar values.
 *
 * The fix: callers pass the offer's UUID string instead of the entity, and
 * this controller resolves it back via the repository. The
 * FragmentReference attribute stays scalar, the renderer stays `'esi'`,
 * and the chain works end-to-end.
 */
final class OfferDetailsController extends AbstractController
{
    public function __invoke(
        string $offer,
        OfferRepositoryInterface $offers,
        Request $request,
        ?string $consentToken = null,
    ): Response {
        $offerEntity = $offers->findByUuid($offer);
        if (!$offerEntity instanceof OfferInterface) {
            throw $this->createNotFoundException();
        }

        // No publish-state guard here on purpose: this detail fragment must
        // serve `unlisted`/`private` offers by direct link (only the public
        // *listing* is filtered to publish='public', in OfferListController) and
        // the back-office "Detailansicht (Web)" preview renders drafts. Access
        // is gated by the /_fragment HMAC signature (the app only emits valid
        // fragment URLs) — keep APP_SECRET strong/rotated (SECURITY_REVIEW FP-013).

        $this->tagResponse(CacheTag::offer($offerEntity));

        $response = $this->render('@Contao/fragment/offer_details_wrapper.html.twig', [
            'offer' => $offerEntity,
            'consentToken' => $consentToken,
        ]);

        // Declare role-bucket cacheability. Without setPublic() + Vary on
        // X-User-Context-Hash, {@see UserContextAwarePrivateResponseListener::isRoleBucketCacheable}
        // has nothing to preserve and falls through to applyContaoDowngrade(),
        // which stamps `private` + `s-maxage=0` and kills Varnish caching.
        //
        // The body contains zero per-user data (the per-user application form
        // is its own ESI fragment, marked private with `Vary: Cookie`), so
        // per-role partitioning is the correct cache key here.
        //
        // `Turbo-Frame` MUST be part of the Vary: the template renders a
        // different body for a modal navigation (`offer_details.html.twig`
        // branches its spacing on `isModal`/`turbo-frame == 'modal'`, and the
        // wrapper is wrapped in `<turbo-frame id="modal">` only then). This
        // fragment is role-bucket cached on its own (ESI) and the main-request
        // {@see UserContextAwarePrivateResponseListener} never sees it, so
        // without this Vary Varnish would serve whichever variant it cached
        // first (modal or direct) to the other — the bug where a plain visit
        // returned the modal-framed body.
        $response->setPublic();
        $response->setVary(['X-User-Context-Hash', 'Turbo-Frame'], false);

        return $response;
    }
}
