<?php

declare(strict_types=1);

namespace App\Module\Catalog\Infrastructure\ApiPlatform\State\Provider;

use ApiPlatform\Doctrine\Orm\Paginator;
use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\ProviderInterface;
use App\Module\Catalog\Domain\Entity\Category;
use App\Module\Catalog\Domain\Repository\CategoryRepositoryInterface;
use Doctrine\ORM\Tools\Pagination\Paginator as DoctrinePaginator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Provider Category : applique le filtre soft-delete par defaut (RG-1.08),
 * accepte ?includeDeleted=true pour inclure les soft-deleted (RG-1.09),
 * trie par name ASC (RG-1.10), et renvoie 404 sur Get d'une soft-deleted
 * sans le flag (RG-1.11, via retour null).
 *
 * Choix d'implementation : QueryBuilder via le repository custom plutot
 * qu'un filtre Doctrine global (cf. spec § 2.3 et arbitrage ticket 0.3).
 * Avantage : pas de magie globale, lisibilite directe du code, controle
 * fin du flag includeDeleted par requete.
 *
 * @implements ProviderInterface<Category>
 */
final class CategoryProvider implements ProviderInterface
{
    public function __construct(
        #[Autowire(service: 'App\Module\Catalog\Infrastructure\Doctrine\DoctrineCategoryRepository')]
        private readonly CategoryRepositoryInterface $repository,
        private readonly Pagination $pagination,
    ) {}

    public function provide(Operation $operation, array $uriVariables = [], array $context = []): Category|iterable|Paginator|null
    {
        $includeDeleted = $this->readIncludeDeleted($context);

        if ($operation instanceof CollectionOperationInterface) {
            $qb = $this->repository->createListQueryBuilder(
                $includeDeleted,
                $this->readTypeCode($context),
                $this->readNameSearch($context),
                $this->readTypeIds($context),
            );

            // Echappatoire ?pagination=false : retourne la collection complete sans Paginator.
            // Utile pour les drawers Role/Permission/Site/CategoryType qui alimentent un <select>.
            if (!$this->pagination->isEnabled($operation, $context)) {
                return $qb->getQuery()->getResult();
            }

            // Branche paginee standard : on applique offset/limit via Pagination,
            // puis on enveloppe dans le Paginator ORM (fetchJoinCollection: true
            // pour que Doctrine compte correctement avec les JOINs futurs).
            $limit  = $this->pagination->getLimit($operation, $context);
            $page   = max(1, $this->pagination->getPage($context));
            $offset = ($page - 1) * $limit;

            $qb->setFirstResult($offset)->setMaxResults($limit);

            return new Paginator(new DoctrinePaginator($qb->getQuery(), fetchJoinCollection: true));
        }

        // Get unitaire : recharger l'entite, puis appliquer le filtre soft-delete.
        $id = $uriVariables['id'] ?? null;
        if (!is_int($id) && !(is_string($id) && ctype_digit($id))) {
            return null;
        }

        $category = $this->repository->findById((int) $id);
        if (null === $category) {
            return null;
        }

        // RG-1.11 : 404 si soft-deleted et pas de flag includeDeleted.
        if (!$includeDeleted && null !== $category->getDeletedAt()) {
            return null;
        }

        return $category;
    }

    /**
     * Lit le flag includeDeleted depuis les filtres API Platform.
     * Accepte "true" / "1" / true (booleen).
     */
    private function readIncludeDeleted(array $context): bool
    {
        $raw = $context['filters']['includeDeleted'] ?? false;

        if (is_bool($raw)) {
            return $raw;
        }

        if (is_string($raw)) {
            return in_array(strtolower($raw), ['true', '1'], true);
        }

        return false;
    }

    /**
     * Lit le filtre `?typeCode=` depuis les filtres API Platform. Renvoie le code
     * normalise (trim) ou null si absent / vide. Ne contraint pas la casse : la
     * comparaison SQL se fait sur le code exact stocke (ex. FOURNISSEUR, CLIENT).
     */
    private function readTypeCode(array $context): ?string
    {
        $raw = $context['filters']['typeCode'] ?? null;

        if (!is_string($raw)) {
            return null;
        }

        $raw = trim($raw);

        return '' === $raw ? null : $raw;
    }

    /**
     * Lit le filtre `?name=` (recherche partielle sur le nom, liste admin).
     * Renvoie la valeur trimmee ou null si absente / vide.
     */
    private function readNameSearch(array $context): ?string
    {
        $raw = $context['filters']['name'] ?? null;

        if (!is_string($raw)) {
            return null;
        }

        $raw = trim($raw);

        return '' === $raw ? null : $raw;
    }

    /**
     * Lit le filtre `?typeId[]=` (liste admin) : ids des types coches (OR).
     * Tolere une valeur scalaire unique (`?typeId=3`) ou un tableau. Ignore
     * les entrees non numeriques.
     *
     * @return list<int>
     */
    private function readTypeIds(array $context): array
    {
        $raw = $context['filters']['typeId'] ?? null;

        if (null === $raw) {
            return [];
        }

        $values = is_array($raw) ? $raw : [$raw];

        $ids = [];
        foreach ($values as $value) {
            if (is_int($value) || (is_string($value) && ctype_digit($value))) {
                $ids[] = (int) $value;
            }
        }

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