<?php

namespace App\Repository;

use App\Entity\ScanSession;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

/**
 * @extends ServiceEntityRepository<ScanSession>
 *
 * Périmètre de visibilité ("scope") : un tableau de concessions, ou null pour "voit tout"
 * (ADMIN/AUDIT). [] signifie "ne voit rien". Le filtre s'applique sur le snapshot JSON
 * `concessions` du scan (LIKE sur la représentation textuelle, comme le filtre concession existant).
 */
class ScanSessionRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, ScanSession::class);
    }

    /**
     * Ajoute la restriction de périmètre sur le QueryBuilder (alias 's').
     *
     * @param list<string>|null $scope
     */
    private function applyScope(QueryBuilder $qb, ?array $scope): void
    {
        if (null === $scope) {
            return; // voit tout
        }
        if ([] === $scope) {
            $qb->andWhere('1 = 0'); // aucune concession → aucun résultat
            return;
        }
        $or = [];
        foreach (array_values($scope) as $i => $name) {
            $or[] = "LOWER(s.concessions) LIKE :sc_$i";
            $qb->setParameter("sc_$i", '%"' . strtolower(trim((string) $name)) . '"%');
        }
        $qb->andWhere('(' . implode(' OR ', $or) . ')');
    }

    /**
     * Les $limit sessions les plus récentes visibles dans le périmètre (non archivées par défaut).
     *
     * @param list<string>|null $scope
     *
     * @return ScanSession[]
     */
    public function findRecentForScope(?array $scope, int $limit = 100, bool $includeArchived = false): array
    {
        $qb = $this->createQueryBuilder('s')
            ->orderBy('s.createdAt', 'DESC')
            ->setMaxResults($limit);
        if (!$includeArchived) {
            $qb->andWhere('s.archivedAt IS NULL');
        }
        $this->applyScope($qb, $scope);

        return $qb->getQuery()->getResult();
    }

    /**
     * Charge un scan par id s'il est visible dans le périmètre (null = tout).
     *
     * @param list<string>|null $scope
     */
    public function findOneInScope(string $id, ?array $scope): ?ScanSession
    {
        $qb = $this->createQueryBuilder('s')
            ->andWhere('s.id = :id')
            ->setParameter('id', $id)
            ->setMaxResults(1);
        $this->applyScope($qb, $scope);

        return $qb->getQuery()->getOneOrNullResult();
    }

    /**
     * Liste des inventaires d'une concession donnée, visibles dans le périmètre (archivés inclus).
     * Si la concession n'est pas dans le périmètre, retourne [].
     *
     * @param list<string>|null $scope
     *
     * @return ScanSession[]
     */
    public function findByConcessionInScope(string $concession, ?array $scope): array
    {
        if (null !== $scope && !\in_array($concession, $scope, true)) {
            return [];
        }

        return $this->createQueryBuilder('s')
            ->andWhere('LOWER(s.concessions) LIKE :conc')
            ->setParameter('conc', '%"' . strtolower(trim($concession)) . '"%')
            ->orderBy('s.createdAt', 'DESC')
            ->getQuery()
            ->getResult();
    }

    /** Charge un scan par id, sans restriction (usage interne / ADMIN). */
    public function findOneById(string $id): ?ScanSession
    {
        return $this->find($id);
    }

    /**
     * Concessions distinctes ayant au moins un inventaire (dans le périmètre), triées.
     * Sert à ne proposer dans la comparaison que des concessions réellement utilisées.
     *
     * @param list<string>|null $scope
     *
     * @return list<string>
     */
    public function concessionsWithInventory(?array $scope): array
    {
        $qb = $this->createQueryBuilder('s')->select('s.concessions');
        $this->applyScope($qb, $scope);

        $names = [];
        foreach ($qb->getQuery()->getArrayResult() as $row) {
            $list = \is_array($row['concessions']) ? $row['concessions'] : [];
            foreach ($list as $c) {
                if (!\is_string($c) || '' === $c) {
                    continue;
                }
                if (null !== $scope && !\in_array($c, $scope, true)) {
                    continue; // concession hors périmètre d'un scan multi-concessions
                }
                $names[$c] = true;
            }
        }

        $names = array_keys($names);
        sort($names);

        return $names;
    }

    /** Scan appartenant à un créateur précis (upsert côté /api/history). */
    public function findOneForUser(string $id, string $email): ?ScanSession
    {
        return $this->findOneBy(['id' => $id, 'userEmail' => $email]);
    }

    /**
     * Les $limit scans créés par un utilisateur (jeu de travail personnel : /api/history, /historique).
     *
     * @return ScanSession[]
     */
    public function findRecentByEmail(string $email, int $limit = 100): array
    {
        return $this->createQueryBuilder('s')
            ->andWhere('s.userEmail = :email')
            ->setParameter('email', $email)
            ->orderBy('s.createdAt', 'DESC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    /** Supprime tous les scans créés par un utilisateur (action "tout effacer"). */
    public function deleteAllForUser(string $email): void
    {
        $this->createQueryBuilder('s')
            ->delete()
            ->andWhere('s.userEmail = :email')
            ->setParameter('email', $email)
            ->getQuery()
            ->execute();
    }

    /**
     * Liste paginée + filtrée pour la page /inventaires, restreinte au périmètre.
     * `$q` est testé contre label, locationNote et JSON results (plaque, VIN, concessions).
     *
     * @param list<string>|null $scope
     * @param 'active'|'archived'|'all' $archived
     *
     * @return array{items: ScanSession[], total: int}
     */
    public function search(
        ?array $scope,
        ?string $q = null,
        ?string $status = null,
        ?string $concession = null,
        int $page = 1,
        int $perPage = 25,
        string $archived = 'active',
        ?string $creator = null,
    ): array {
        $qb = $this->searchQb($scope, $q, $status, $concession, $archived, $creator);

        $totalQb = clone $qb;
        $total = (int) $totalQb->select('COUNT(s.id)')->resetDQLPart('orderBy')->getQuery()->getSingleScalarResult();

        $items = $qb
            ->setFirstResult(max(0, ($page - 1) * $perPage))
            ->setMaxResults($perPage)
            ->getQuery()
            ->getResult();

        return ['items' => $items, 'total' => $total];
    }

    /**
     * QueryBuilder commun aux listes filtrées (page /inventaires), restreint au périmètre.
     *
     * @param list<string>|null $scope
     * @param 'active'|'archived'|'all' $archived
     */
    private function searchQb(
        ?array $scope,
        ?string $q,
        ?string $status,
        ?string $concession,
        string $archived,
        ?string $creator,
    ): QueryBuilder {
        $qb = $this->createQueryBuilder('s')->orderBy('s.createdAt', 'DESC');
        $this->applyScope($qb, $scope);

        if ('active' === $archived) {
            $qb->andWhere('s.archivedAt IS NULL');
        } elseif ('archived' === $archived) {
            $qb->andWhere('s.archivedAt IS NOT NULL');
        }

        if (null !== $status && '' !== $status) {
            $qb->andWhere('s.status = :st')->setParameter('st', $status);
        }
        if (null !== $q && '' !== $q) {
            $qb->andWhere('(LOWER(s.label) LIKE :q OR LOWER(s.locationNote) LIKE :q OR LOWER(s.results) LIKE :q)')
                ->setParameter('q', '%' . strtolower($q) . '%');
        }
        if (null !== $concession && '' !== $concession) {
            $qb->andWhere('LOWER(s.concessions) LIKE :conc')
                ->setParameter('conc', '%"' . strtolower($concession) . '"%');
        }
        if (null !== $creator && '' !== $creator) {
            $qb->andWhere('s.userEmail = :creator')->setParameter('creator', $creator);
        }

        return $qb;
    }

    /**
     * Emails distincts des créateurs de scans visibles dans le périmètre (options du filtre "Créateur").
     *
     * @param list<string>|null $scope
     *
     * @return list<string>
     */
    public function distinctCreatorsForScope(?array $scope): array
    {
        $qb = $this->createQueryBuilder('s')
            ->select('DISTINCT s.userEmail AS email')
            ->orderBy('email', 'ASC');
        $this->applyScope($qb, $scope);

        return array_column($qb->getQuery()->getScalarResult(), 'email');
    }

    /**
     * Compte par statut pour l'année donnée (non archivés), dans le périmètre.
     *
     * @param list<string>|null $scope
     *
     * @return array<string, int>
     */
    public function countByStatusForScope(?array $scope, int $year): array
    {
        [$start, $end] = $this->yearRange($year);

        $qb = $this->createQueryBuilder('s')
            ->select('s.status, COUNT(s.id) AS nb')
            ->andWhere('s.archivedAt IS NULL')
            ->andWhere('s.createdAt >= :start AND s.createdAt < :end')
            ->setParameter('start', $start)
            ->setParameter('end', $end)
            ->groupBy('s.status');
        $this->applyScope($qb, $scope);

        $out = [];
        foreach (ScanSession::ALL_STATUSES as $st) {
            $out[$st] = 0;
        }
        foreach ($qb->getQuery()->getArrayResult() as $r) {
            $out[$r['status']] = (int) $r['nb'];
        }

        return $out;
    }

    /**
     * Comptes par mois (1..12) pour l'année (non archivés), dans le périmètre.
     *
     * @param list<string>|null $scope
     *
     * @return array<int, int>
     */
    public function countByMonthForScope(?array $scope, int $year): array
    {
        [$start, $end] = $this->yearRange($year);

        $qb = $this->createQueryBuilder('s')
            ->select('s.createdAt')
            ->andWhere('s.archivedAt IS NULL')
            ->andWhere('s.createdAt >= :start AND s.createdAt < :end')
            ->setParameter('start', $start)
            ->setParameter('end', $end);
        $this->applyScope($qb, $scope);

        $out = array_fill(1, 12, 0);
        foreach ($qb->getQuery()->getArrayResult() as $row) {
            /** @var \DateTimeImmutable $dt */
            $dt = $row['createdAt'];
            $m = (int) $dt->format('n');
            $out[$m] = ($out[$m] ?? 0) + 1;
        }

        return $out;
    }

    /**
     * Comptes par mois ET par statut pour l'année (non archivés), dans le périmètre.
     *
     * @param list<string>|null $scope
     *
     * @return array<string, array<int, int>>
     */
    public function countByMonthAndStatusForScope(?array $scope, int $year): array
    {
        [$start, $end] = $this->yearRange($year);

        $qb = $this->createQueryBuilder('s')
            ->select('s.status, s.createdAt')
            ->andWhere('s.archivedAt IS NULL')
            ->andWhere('s.createdAt >= :start AND s.createdAt < :end')
            ->setParameter('start', $start)
            ->setParameter('end', $end);
        $this->applyScope($qb, $scope);

        $out = [];
        foreach (ScanSession::ALL_STATUSES as $st) {
            $out[$st] = array_fill(1, 12, 0);
        }
        foreach ($qb->getQuery()->getArrayResult() as $row) {
            /** @var \DateTimeImmutable $dt */
            $dt = $row['createdAt'];
            $m = (int) $dt->format('n');
            $st = $row['status'];
            if (isset($out[$st][$m])) {
                ++$out[$st][$m];
            }
        }

        return $out;
    }

    /**
     * Statistiques agrégées pour le dashboard (non archivés), dans le périmètre.
     *
     * @param list<string>|null $scope
     *
     * @return array{sessions: int, vehicles: int, lastAt: ?\DateTimeImmutable, files: int}
     */
    public function statsForScope(?array $scope, int $year): array
    {
        [$start, $end] = $this->yearRange($year);

        $qb = $this->createQueryBuilder('s')
            ->select('COUNT(s.id) AS sessions, COALESCE(SUM(s.count), 0) AS vehicles, COALESCE(SUM(s.totalFiles), 0) AS files, MAX(s.createdAt) AS lastAt')
            ->andWhere('s.archivedAt IS NULL')
            ->andWhere('s.createdAt >= :start AND s.createdAt < :end')
            ->setParameter('start', $start)
            ->setParameter('end', $end);
        $this->applyScope($qb, $scope);

        $row = $qb->getQuery()->getSingleResult();

        return [
            'sessions' => (int) $row['sessions'],
            'vehicles' => (int) $row['vehicles'],
            'files' => (int) $row['files'],
            'lastAt' => $row['lastAt'] instanceof \DateTimeImmutable ? $row['lastAt'] : null,
        ];
    }

    /**
     * Top concessions sur l'année (non archivés), dans le périmètre.
     * Si un périmètre est donné, seules les concessions du périmètre sont comptées.
     *
     * @param list<string>|null $scope
     *
     * @return array<string, int>
     */
    public function topConcessionsForScope(?array $scope, int $year, int $limit = 5): array
    {
        [$start, $end] = $this->yearRange($year);

        $qb = $this->createQueryBuilder('s')
            ->select('s.concessions')
            ->andWhere('s.archivedAt IS NULL')
            ->andWhere('s.createdAt >= :start AND s.createdAt < :end')
            ->setParameter('start', $start)
            ->setParameter('end', $end);
        $this->applyScope($qb, $scope);

        $counts = [];
        foreach ($qb->getQuery()->getArrayResult() as $row) {
            $list = \is_array($row['concessions']) ? $row['concessions'] : [];
            foreach (array_unique($list) as $c) {
                if (!\is_string($c) || '' === $c) {
                    continue;
                }
                if (null !== $scope && !\in_array($c, $scope, true)) {
                    continue; // ne pas compter les concessions hors périmètre d'un scan multi-concessions
                }
                $counts[$c] = ($counts[$c] ?? 0) + 1;
            }
        }

        arsort($counts);

        return \array_slice($counts, 0, $limit, true);
    }

    /**
     * @return array{0: \DateTimeImmutable, 1: \DateTimeImmutable}
     */
    private function yearRange(int $year): array
    {
        $start = new \DateTimeImmutable(sprintf('%d-01-01 00:00:00', $year));

        return [$start, $start->modify('+1 year')];
    }
}
