<?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\Attendance;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\Debtor;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\Entity\User;
use Symfony\Component\Uid\Uuid;

/**
 * Single source of truth for Varnish xkey cache tags.
 *
 * Using UUIDs (not incrementing ids) keeps tags stable across fixture
 * rebuilds and avoids collisions between independently-scoped entities.
 * Controllers tag responses with these; Doctrine invalidators purge them.
 */
final class CacheTag
{
    public static function offer(OfferInterface|Uuid|string $offer): string
    {
        return 'offer.'.self::uuid($offer instanceof OfferInterface ? $offer->getUuid() : $offer);
    }

    public static function offerCollection(Edition|Uuid|string|null $edition = null): string
    {
        if (null === $edition) {
            return 'offer.collection';
        }

        return 'offer.collection.'.self::uuid($edition instanceof Edition ? $edition->getUuid() : $edition);
    }

    public static function host(Host|Uuid|string $host): string
    {
        return 'host.'.self::uuid($host instanceof Host ? $host->getUuid() : $host);
    }

    public static function participant(ParticipantInterface|Uuid|string $participant): string
    {
        return 'participant.'.self::uuid($participant instanceof ParticipantInterface ? $participant->getUuid() : $participant);
    }

    public static function user(User|Uuid|string $user): string
    {
        return 'user.'.self::uuid($user instanceof User ? $user->getUuid() : $user);
    }

    public static function edition(Edition|Uuid|string $edition): string
    {
        return 'edition.'.self::uuid($edition instanceof Edition ? $edition->getUuid() : $edition);
    }

    public static function attendance(Attendance|Uuid|string $attendance): string
    {
        return 'attendance.'.self::uuid($attendance instanceof Attendance ? $attendance->getUuid() : $attendance);
    }

    public static function payment(Payment|Uuid|string $payment): string
    {
        return 'payment.'.self::uuid($payment instanceof Payment ? $payment->getUuid() : $payment);
    }

    public static function debtor(Debtor|Uuid|string $debtor): string
    {
        return 'debtor.'.self::uuid($debtor instanceof Debtor ? $debtor->getUuid() : $debtor);
    }

    public static function consent(Consent|Uuid|string $consent): string
    {
        return 'consent.'.self::uuid($consent instanceof Consent ? $consent->getUuid() : $consent);
    }

    /**
     * The per-season host confidentiality declaration ("Einverständnis der
     * Veranstaltenden", {@see Edition::getHostConsentForm()}).
     *
     * Carried by every offer's participant list while the edition gates it
     * ({@see \Ferienpass\AdminBundle\Controller\Page\OfferParticipantsController},
     * `missing_privacy_statement.html.twig`), so signing, revoking or deleting a
     * host's signature — or editing the declaration text — re-evaluates the gate
     * on all of them. Keyed by the edition (the stable anchor: the host
     * ConsentForm is recreated when the declaration is toggled off and on again),
     * distinct from {@see self::edition()} so ordinary edition churn (attendance
     * status changes, edition-task edits) doesn't evict the participant lists.
     */
    public static function hostConsent(Edition|Uuid|string $edition): string
    {
        return 'host-consent.'.self::uuid($edition instanceof Edition ? $edition->getUuid() : $edition);
    }

    /**
     * Aggregate tag for the staged-attendances count badge (the ESI fragment in
     * the admin header). The count is a tenant-global aggregate with no entity
     * UUID, so it's keyed by the kernel's `app.id`.
     *
     * Scoping caveat: `app.id` is the TENANT id only on isolated builds. On the
     * shared build the general kernel runs with `app.id = ''` (the tenant is
     * resolved per-request from the Host — see Shared\Tenant\CurrentTenant), so
     * every tenant shares the `stash.` tag and one tenant's purge evicts all of
     * their badges. That over-invalidation is accepted by design: Varnish still
     * keys the fragment by Host, so each tenant just re-renders its own count (a
     * cheap COUNT) on the next request — no stale data, no cross-tenant leak,
     * only a little redundant churn. A host-keyed tag would avoid the churn but
     * would have to thread the Host through every stash-mutating worker.
     */
    public static function stash(string $appId): string
    {
        return 'stash.'.$appId;
    }

    /**
     * Aggregate tag for the admin dashboard (`/admin`, route `admin_index`).
     *
     * The dashboard renders attendance-derived aggregates — the heatmap
     * (`COUNT(*)` of attendances per day), the next-steps widget
     * (editions with uncommitted / waiting attendances) and the recent-activity
     * feed (ParticipantLog rows written on each application). None of these carry
     * a single entity UUID, so — like {@see self::stash()} — the page is keyed by
     * the kernel's `app.id` and purged whenever an attendance's lifecycle moves
     * one of those aggregates. The same `app.id` scoping caveat as `stash()`
     * applies on the shared build (harmless over-invalidation, no stale data).
     */
    public static function dashboard(string $appId): string
    {
        return 'dashboard.'.$appId;
    }

    /**
     * Aggregate tag for the Postausgang (`/admin/postausgang`, route
     * `admin_outbox`).
     *
     * The outbox lists every sent email and its conversation thread, none of
     * which carry a single entity UUID the page could tag itself with — so, like
     * {@see self::stash()} and {@see self::dashboard()}, it's keyed by the
     * kernel's `app.id` and purged whenever a {@see \Ferienpass\CoreBundle\Entity\SentMessage}
     * is logged (a new row) or a {@see \Ferienpass\CoreBundle\Entity\ConversationItem}
     * is written (a new comment/reply changes the thread + reply badges).
     *
     * Crucially host-less: the mail is logged from the async worker, which has no
     * request host for a BAN — an xkey purge needs none, and
     * {@see \Ferienpass\CoreBundle\EventListener\HttpCache\FlushCacheManagerOnWorkerMessageListener}
     * flushes it. The same `app.id` scoping caveat as `stash()` applies on the
     * shared build (harmless over-invalidation, no stale data, no cross-tenant leak).
     */
    public static function outbox(string $appId): string
    {
        return 'outbox.'.$appId;
    }

    private static function uuid(Uuid|string $uuid): string
    {
        return $uuid instanceof Uuid ? $uuid->toRfc4122() : $uuid;
    }
}
