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

use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\User;
use FOS\HttpCache\UserContext\ContextProvider;
use FOS\HttpCache\UserContext\UserContext;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

/**
 * Builds the user-context hash used by FOSHttpCacheBundle / Varnish to
 * partition cached responses into role buckets.
 *
 * The application has two stateful firewalls that need to contribute to the
 * hash — `contao_frontend` and `ferienpass_admin`. We don't consolidate them
 * inside this provider; instead, Varnish side-requests a different hash URL
 * depending on the original request path:
 *
 *   - /admin/*  →  /admin/_fos_user_context_hash  (ferienpass_admin firewall)
 *   - /*        →  /_fos_user_context_hash        (contao_frontend firewall)
 *
 * See varnish/config/fos_user_context_url.vcl. Each firewall authenticates
 * its own URL, so by the time this provider runs `TokenStorageInterface`
 * already holds the right token — no custom session scanning needed.
 *
 * ### Why role-bucketing (and not per-user)
 *
 * The cache key intentionally collapses every user with the same role
 * profile into one bucket, so the public offer list / homepage / category
 * pages are reused across users. Personal data (name, e-mail, wishlist
 * affordances) lives in ESI sub-fragments that Varnish fetches separately
 * per session — see {@see \Ferienpass\CmsBundle\Controller\Fragment\UserNavigationController}.
 *
 * If you find yourself wanting to add per-user fields here to "fix" a
 * leak, the leak is almost certainly somewhere else: a render_esi() call
 * is inline-falling-back (Surrogate-Capability not on the request), a
 * page template is reading `app.user.*` outside an ESI fragment, or a
 * controller is forgetting to extend
 * {@see \Ferienpass\CoreBundle\Controller\AbstractPrivateFragmentController}.
 * Fix the leak at its source, not by exploding the cache key.
 */
class UserContextProvider implements ContextProvider
{
    public function __construct(
        private readonly TokenStorageInterface $tokenStorage,
    ) {
    }

    public function updateUserContext(UserContext $context): void
    {
        $token = $this->tokenStorage->getToken();
        $user = $this->resolveUser($token);

        $context->addParameter('roles', $this->normalisedRoles($user));

        // SwitchUserToken means an admin used `switch_user` to impersonate
        // another user. The page template emits the impersonation banner ESI
        // call only when this flag is true (see class docblock).
        if ($token instanceof SwitchUserToken) {
            $context->addParameter('impersonator', true);
        }

        if (null === $user) {
            return;
        }

        if ($user->isHost() && !$user->isAdmin()) {
            $hosts = $user->getHosts()
                ->map(static fn (Host $host) => $host->getUuid()->toRfc4122())
                ->toArray();
            sort($hosts);
            $context->addParameter('hosts', $hosts);
        }
    }

    private function resolveUser(?TokenInterface $token): ?User
    {
        if (!$token instanceof TokenInterface) {
            return null;
        }

        $user = $token->getUser();

        return $user instanceof User ? $user : null;
    }

    /**
     * @return list<string>
     */
    private function normalisedRoles(?User $user): array
    {
        if (null === $user) {
            return [];
        }

        $roles = $user->getRoles();
        sort($roles);

        return array_values(array_unique($roles));
    }
}
