<?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 les collections et items de la
 * resource Site (/api/sites) aux seuls sites auxquels l'utilisateur
 * authentifie est rattache (ticket module Sites — prevention de la fuite
 * de donnees cross-tenant).
 *
 * `Site` n'implemente pas `SiteAwareInterface` (ce serait circulaire : un
 * site ne s'appartient pas a lui-meme). Cette extension complementaire
 * cible specifiquement `Site::class` et applique un filtre IN sur les IDs
 * des sites de l'utilisateur.
 *
 * Comportement selon les cas :
 *  - resource != Site::class          → no-op (les autres resources sont
 *                                        gerees par SiteScopedQueryExtension) ;
 *  - is_granted('sites.bypass_scope') → pas de filtre (admin / bypass) ;
 *  - is_granted('sites.read_ref')     → pas de filtre (lecture-referentiel
 *                                        transverse complet, ERP-102) ;
 *  - user non authentifie             → no-op (API Platform renvoie 401 avant) ;
 *  - user sans aucun site             → WHERE 1 = 0 (aucun acces) ;
 *  - cas normal                       → WHERE site.id IN (:allowedSites).
 *
 * Consequence anti-enumeration : GET /api/sites/{id} retourne 404 et non
 * 403 si l'item existe mais n'appartient pas aux sites de l'utilisateur
 * (comportement natif API Platform quand Doctrine retourne null).
 */
final class SiteCollectionScopedExtension 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 IN sur les IDs de sites autorises 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 Site.
        if (Site::class !== $resourceClass) {
            return;
        }

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

        // 2bis) Lecture-referentiel transverse (ERP-102) : `sites.read_ref` donne
        // acces a la LISTE COMPLETE des sites (selects d'adresse des modules Tiers).
        // Sans ce bypass, le cloisonnement par site rattache reduirait le select
        // aux seuls sites de l'utilisateur (voire a rien s'il n'en a aucun) et le
        // referentiel ne serait plus "transverse". `read_ref` est une lecture seule :
        // il ouvre la visibilite sans permettre la moindre ecriture.
        if ($this->security->isGranted('sites.read_ref')) {
            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];

        // 4) User sans aucun site rattache -> aucun acces possible.
        $siteIds = $user->getSites()->map(fn (Site $s) => $s->getId())->toArray();
        if (empty($siteIds)) {
            $queryBuilder->andWhere('1 = 0');

            return;
        }

        // 5) Cas normal : restriction aux sites autorises de l'utilisateur.
        $param = $queryNameGenerator->generateParameterName('allowedSites');
        $queryBuilder
            ->andWhere(sprintf('%s.id IN (:%s)', $rootAlias, $param))
            ->setParameter($param, $siteIds)
        ;
    }
}
