<?php

declare(strict_types=1);

namespace App\Module\Transport\Infrastructure\Doctrine;

use App\Module\Transport\Domain\Entity\Carrier;
use App\Module\Transport\Domain\Repository\CarrierRepositoryInterface;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<Carrier>
 */
class DoctrineCarrierRepository extends ServiceEntityRepository implements CarrierRepositoryInterface
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Carrier::class);
    }

    public function findById(int $id): ?Carrier
    {
        return $this->find($id);
    }

    public function save(Carrier $carrier): void
    {
        $this->getEntityManager()->persist($carrier);
        $this->getEntityManager()->flush();
    }

    public function createListQueryBuilder(
        bool $includeArchived = false,
        ?string $search = null,
        array $certificationTypes = [],
        bool $archivedOnly = false,
    ): QueryBuilder {
        // Fetch-join de la SEULE relation ManyToOne qualimatCarrier (sur, pas de
        // cartesien) pour exposer statut/date de validite QUALIMAT en liste sans
        // N+1 (§ 2.11). Aucune sous-collection (addresses/contacts/prices) jointe
        // en liste : elles ne sont embarquees qu'au detail (carrier:item:read).
        $qb = $this->createQueryBuilder('c')
            ->leftJoin('c.qualimatCarrier', 'q')->addSelect('q')
            ->andWhere('c.deletedAt IS NULL')
            ->orderBy('c.name', 'ASC')
        ;

        // Pas de cloisonnement par site (§ 2.3) : referentiel global.
        // Perimetre d'archivage : archivedOnly prioritaire sur includeArchived
        // (jumeau de DoctrineProviderRepository — toggle « Voir les archives »).
        if ($archivedOnly) {
            $qb->andWhere('c.isArchived = true');
        } elseif (!$includeArchived) {
            $qb->andWhere('c.isArchived = false');
        }

        $this->applySearch($qb, $search);
        $this->applyCertificationTypes($qb, $certificationTypes);

        return $qb;
    }

    /**
     * Recherche fuzzy insensible a la casse sur le nom du transporteur (§ 4.1).
     * Metacaracteres LIKE (%, _, \) echappes pour rester litteraux.
     */
    private function applySearch(QueryBuilder $qb, ?string $search): void
    {
        if (null === $search || '' === trim($search)) {
            return;
        }

        $escaped = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], trim($search));
        $pattern = '%'.mb_strtolower($escaped, 'UTF-8').'%';

        $qb->andWhere('LOWER(c.name) LIKE :search')
            ->setParameter('search', $pattern)
        ;
    }

    /**
     * Restreint aux transporteurs dont la certification figure dans la liste (OR).
     * Alimente le filtre « Certification » de la liste (§ 4.1).
     *
     * @param list<string> $certificationTypes
     */
    private function applyCertificationTypes(QueryBuilder $qb, array $certificationTypes): void
    {
        $codes = [];
        foreach ($certificationTypes as $code) {
            if (is_string($code) && '' !== trim($code)) {
                $codes[] = trim($code);
            }
        }

        if ([] === $codes) {
            return;
        }

        $qb->andWhere('c.certificationType IN (:certificationTypes)')
            ->setParameter('certificationTypes', $codes)
        ;
    }
}
