<?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\Core\Domain\Entity\User;
use App\Module\Sites\Domain\Entity\Site;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;

use function sprintf;

/**
 * Extension API Platform qui restreint /api/users (collection + item) aux
 * utilisateurs partageant au moins un site commun avec l'appelant.
 *
 * Objectif : empecher l'enumeration cross-site des utilisateurs. Sans ce
 * filtre, un user du site A pourrait lister tous les users du site B via
 * GET /api/users.
 *
 * Conditions de bypass :
 *  - is_granted('sites.bypass_scope') → visibilite totale (admin ou bypass
 *    explicite) ;
 *  - user non authentifie             → no-op (API Platform renvoie 401) ;
 *
 * Cas particulier — appelant sans aucun site rattache :
 *  Comportement defensif : l'utilisateur ne voit que lui-meme. Cela evite
 *  de bloquer completement un user mal configure tout en ne lui revelant
 *  aucun autre utilisateur.
 *
 * Strategie DQL : JOIN sur la relation ManyToMany `.sites` + DISTINCT pour
 * eviter les doublons si un user partage plusieurs sites avec l'appelant.
 * Le alias `s_scope` est utilise pour la jointure intermediaire.
 */
final class UserSiteScopedExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
    public function __construct(
        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);
    }

    /**
     * Applique le filtre de partage de site si les conditions d'application
     * sont remplies. No-op sinon.
     */
    private function applyScope(
        QueryBuilder $queryBuilder,
        QueryNameGeneratorInterface $queryNameGenerator,
        string $resourceClass,
    ): void {
        // 1) Cette extension cible uniquement la resource User.
        if (User::class !== $resourceClass) {
            return;
        }

        // 2) Admin ou bypass explicite : visibilite totale.
        if ($this->security->isGranted('sites.bypass_scope')) {
            return;
        }

        // 3) Pas d'user authentifie -> no-op (API Platform gere le 401 en amont).
        $user = $this->security->getUser();
        if (!$user instanceof User) {
            return;
        }

        $rootAlias     = $queryBuilder->getRootAliases()[0];
        $callerSiteIds = $user->getSites()->map(fn (Site $s) => $s->getId())->toArray();

        // 4) Appelant sans site : comportement defensif -> il ne voit que lui-meme.
        if (empty($callerSiteIds)) {
            $queryBuilder
                ->andWhere(sprintf('%s.id = :self', $rootAlias))
                ->setParameter('self', $user->getId())
            ;

            return;
        }

        // 5) Cas normal : garder uniquement les users qui partagent au moins
        //    un site avec l'appelant. JOIN sur la relation ManyToMany `.sites`
        //    + filtre IN + DISTINCT pour eviter les lignes dupliquees.
        $param = $queryNameGenerator->generateParameterName('callerSites');
        $queryBuilder
            ->innerJoin(sprintf('%s.sites', $rootAlias), 's_scope')
            ->andWhere(sprintf('s_scope.id IN (:%s)', $param))
            ->setParameter($param, $callerSiteIds)
            ->distinct()
        ;
    }
}
