<?php

declare(strict_types=1);

namespace App\Module\Sites\Infrastructure\ApiPlatform\Extension;

use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
use App\Shared\Domain\Contract\SiteAwareInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;

use function is_subclass_of;
use function sprintf;

/**
 * Extension API Platform qui filtre automatiquement les collections et les
 * items des resources implementant SiteAwareInterface selon le site
 * courant de l'utilisateur authentifie (ticket 4 module Sites).
 *
 * Appliquee automatiquement par API Platform sur toutes les requetes GET
 * (collection + item), mais devient no-op si :
 *  - la resource cible n'implemente pas SiteAwareInterface ;
 *  - l'user a la permission `sites.bypass_scope` ;
 *  - CurrentSiteProvider::get() retourne null (module desactive, pas
 *    d'user authentifie, ou user sans currentSite).
 *
 * Le filtrage est identique pour les deux interfaces Collection et Item,
 * factorise dans `applyScope()`. Consequence sur GET /api/resource/{id} :
 * si l'item existe en base mais appartient a un autre site, Doctrine
 * retourne null apres filtrage et API Platform converti en 404
 * (anti-enumeration : le user ne peut pas distinguer "n'existe pas" de
 * "appartient a un autre site").
 */
final class SiteScopedQueryExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    public function __construct(
        private readonly CurrentSiteProviderInterface $currentSiteProvider,
        private readonly Security $security,
    ) {}

    public function applyToCollection(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        ?Operation $operation = null,
        array $context = [],
    ): void {
        $this->applyScope($queryBuilder, $queryNameGenerator, $resourceClass);
    }

    public function applyToItem(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
        array $identifiers,
        ?Operation $operation = null,
        array $context = [],
    ): void {
        $this->applyScope($queryBuilder, $queryNameGenerator, $resourceClass);
    }

    /**
     * Ajoute la clause `WHERE <alias>.site = :currentSite` au query builder
     * si les 3 conditions d'application sont remplies. No-op sinon.
     */
    private function applyScope(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
    ): void {
        // 1) Filtrer uniquement les resources qui ont opt-in via l'interface.
        //    `is_subclass_of` gere a la fois `implements` direct et herite.
        if (!is_subclass_of($resourceClass, SiteAwareInterface::class)) {
            return;
        }

        // 2) Admin ou user avec bypass explicite : visibilite globale.
        //    is_granted('sites.bypass_scope') retourne true pour les admins
        //    (bypass total via isAdmin) meme sans permission explicite.
        if ($this->security->isGranted('sites.bypass_scope')) {
            return;
        }

        // 3) Pas de site courant -> no-op plutot que collection vide.
        //    Decision assumee (cf. ticket 4 spec Risque 1) : un user sans
        //    currentSite voit tout. L'alternative "collection vide" est
        //    rejetee car elle rendrait l'app inutilisable pour un user
        //    mal configure.
        $currentSite = $this->currentSiteProvider->get();
        if (null === $currentSite) {
            return;
        }

        // Application du filtre : alias racine du QueryBuilder, parametre
        // genere pour eviter les collisions avec d'autres extensions.
        $rootAlias     = $queryBuilder->getRootAliases()[0];
        $parameterName = $queryNameGenerator->generateParameterName('currentSite');

        $queryBuilder
            ->andWhere(sprintf('%s.site = :%s', $rootAlias, $parameterName))
            ->setParameter($parameterName, $currentSite)
        ;
    }
}
