<?php

declare(strict_types=1);

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Marketing\Repository;

use CpCreation\VitiCore\Marketing\Model\ProductReview;
use CpCreation\VitiCore\Marketing\Model\ReviewInvitation;
use CpCreation\VitiCore\Marketing\ProductReviewRatingSummary;
use CpCreation\VitiCore\Marketing\PublishedProductReviewRatingCollectionProvider;
use CpCreation\VitiCore\Marketing\PublishedProductReviewRatingProvider;
use CpCreation\VitiCore\Marketing\PublishedProductReviewsProvider;
use CpCreation\VitiCore\Marketing\ReviewPublicationPolicy;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Repository\BaseRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;

final class ProductReviewRepository extends BaseRepository implements PublishedProductReviewRatingCollectionProvider, PublishedProductReviewRatingProvider, PublishedProductReviewsProvider
{
    /** @throws \ReflectionException */
    public function __construct(
        ManagerRegistry $registry,
        private readonly ReviewPublicationPolicy $publicationPolicy,
    ) {
        parent::__construct($registry, ProductReview::class);
    }

    /** @return list<ProductReview> */
    public function findLatestForAdmin(int $limit = 100): array
    {
        return $this->createAdminQueryBuilder()
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    public function createAdminQueryBuilder(
        ?string $search = null,
        ?string $productId = null,
        ?int $rating = null,
        ?string $status = null,
    ): QueryBuilder {
        $queryBuilder = $this->createQueryBuilder('review')
            ->addSelect('product', 'invitation')
            ->addSelect('CASE WHEN review.status = :pendingStatus THEN 0 ELSE 1 END AS HIDDEN statusPriority')
            ->innerJoin('review.product', 'product')
            ->innerJoin('review.invitation', 'invitation')
            ->setParameter('pendingStatus', ProductReview::STATUS_PENDING)
            ->orderBy('statusPriority', 'ASC')
            ->addOrderBy('review.submittedAt', 'DESC');

        $search = null !== $search ? mb_strtolower(mb_trim($search)) : '';
        if ('' !== $search) {
            $queryBuilder
                ->distinct()
                ->leftJoin('product.translations', 'productTranslation')
                ->andWhere(<<<'DQL'
                    LOWER(COALESCE(review.authorDisplayName, '')) LIKE :reviewSearch
                    OR LOWER(COALESCE(review.title, '')) LIKE :reviewSearch
                    OR LOWER(COALESCE(review.comment, '')) LIKE :reviewSearch
                    OR LOWER(COALESCE(review.merchantReply, '')) LIKE :reviewSearch
                    OR LOWER(COALESCE(productTranslation.header, '')) LIKE :reviewSearch
                    OR LOWER(COALESCE(productTranslation.name, '')) LIKE :reviewSearch
                    OR LOWER(COALESCE(productTranslation.quality, '')) LIKE :reviewSearch
                    DQL)
                ->setParameter('reviewSearch', '%' . $search . '%');
        }

        if (null !== $productId) {
            $queryBuilder
                ->andWhere('product.id = :reviewProductId')
                ->setParameter('reviewProductId', $productId);
        }

        if (null !== $rating && $rating >= 1 && $rating <= 5) {
            $queryBuilder
                ->andWhere('review.rating = :reviewRating')
                ->setParameter('reviewRating', $rating);
        }

        if (\in_array($status, [
            ProductReview::STATUS_PENDING,
            ProductReview::STATUS_PUBLISHED,
            ProductReview::STATUS_REJECTED,
            ProductReview::STATUS_WITHDRAWN,
        ], true)) {
            $queryBuilder
                ->andWhere('review.status = :reviewStatus')
                ->setParameter('reviewStatus', $status);
        }

        return $queryBuilder;
    }

    public function countPendingForAdmin(): int
    {
        return (int) $this->createQueryBuilder('review')
            ->select('COUNT(review.id)')
            ->andWhere('review.status = :status')
            ->setParameter('status', ProductReview::STATUS_PENDING)
            ->getQuery()
            ->getSingleScalarResult();
    }

    /** @return list<Product> */
    public function findProductsForAdminFilter(): array
    {
        /** @var list<array{productId: string}> $productRows */
        $productRows = $this->createQueryBuilder('review')
            ->select('IDENTITY(review.product) AS productId')
            ->distinct()
            ->getQuery()
            ->getArrayResult();
        $productIds = array_column($productRows, 'productId');
        if ([] === $productIds) {
            return [];
        }

        /** @var list<Product> $products */
        $products = $this->getEntityManager()
            ->getRepository(Product::class)
            ->findBy(['id' => $productIds]);

        usort(
            $products,
            static fn(Product $first, Product $second): int => strnatcasecmp($first->getFullName(), $second->getFullName()),
        );

        return $products;
    }

    /** @return list<ProductReview> */
    public function findPendingSubmittedBefore(\DateTimeImmutable $deadline, int $limit = 500): array
    {
        /** @var list<ProductReview> $reviews */
        $reviews = $this->createQueryBuilder('review')
            ->andWhere('review.status = :status')
            ->andWhere('review.submittedAt <= :deadline')
            ->setParameter('status', ProductReview::STATUS_PENDING)
            ->setParameter('deadline', $deadline)
            ->orderBy('review.submittedAt', 'ASC')
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();

        return $reviews;
    }

    public function getPublishedRatingSummary(
        Product $product,
        ?\DateTimeImmutable $now = null,
    ): ?ProductReviewRatingSummary {
        $now ??= new \DateTimeImmutable();

        /**
         * @var array{
         *     reviewCount: int|string,
         *     ratingValue: float|string|null,
         *     rating1Count: int|string,
         *     rating2Count: int|string,
         *     rating3Count: int|string,
         *     rating4Count: int|string,
         *     rating5Count: int|string
         * } $result
         */
        $result = $this->createPublishedByProductQueryBuilder($product, $now)
            ->select('COUNT(review.id) AS reviewCount')
            ->addSelect('AVG(review.rating) AS ratingValue')
            ->addSelect('SUM(CASE WHEN review.rating = 1 THEN 1 ELSE 0 END) AS rating1Count')
            ->addSelect('SUM(CASE WHEN review.rating = 2 THEN 1 ELSE 0 END) AS rating2Count')
            ->addSelect('SUM(CASE WHEN review.rating = 3 THEN 1 ELSE 0 END) AS rating3Count')
            ->addSelect('SUM(CASE WHEN review.rating = 4 THEN 1 ELSE 0 END) AS rating4Count')
            ->addSelect('SUM(CASE WHEN review.rating = 5 THEN 1 ELSE 0 END) AS rating5Count')
            ->getQuery()
            ->getSingleResult();

        $reviewCount = (int) $result['reviewCount'];
        if (0 === $reviewCount || null === $result['ratingValue']) {
            return null;
        }

        return new ProductReviewRatingSummary(
            reviewCount: $reviewCount,
            ratingValue: (float) $result['ratingValue'],
            ratingCounts: [
                1 => (int) $result['rating1Count'],
                2 => (int) $result['rating2Count'],
                3 => (int) $result['rating3Count'],
                4 => (int) $result['rating4Count'],
                5 => (int) $result['rating5Count'],
            ],
        );
    }

    public function getPublishedRatingSummaries(
        iterable $products,
        ?\DateTimeImmutable $now = null,
    ): array {
        $products = \is_array($products) ? array_values($products) : iterator_to_array($products, false);
        if ([] === $products) {
            return [];
        }

        $now ??= new \DateTimeImmutable();

        /**
         * @var list<array{
         *     productId: object|string,
         *     reviewCount: int|string,
         *     ratingValue: float|string
         * }> $results
         */
        $results = $this->createQueryBuilder('review')
            ->select('IDENTITY(review.product) AS productId')
            ->addSelect('COUNT(review.id) AS reviewCount')
            ->addSelect('AVG(review.rating) AS ratingValue')
            ->andWhere('review.product IN (:products)')
            ->andWhere('review.status = :status')
            ->andWhere('review.publishedAt BETWEEN :publishedSince AND :publishedUntil')
            ->setParameter('products', $products)
            ->setParameter('status', ProductReview::STATUS_PUBLISHED)
            ->setParameter('publishedSince', $this->publicationPolicy->publicSince($now))
            ->setParameter('publishedUntil', $now)
            ->groupBy('review.product')
            ->getQuery()
            ->getArrayResult();

        $summaries = [];
        foreach ($results as $result) {
            $productId = $result['productId'];
            $productId = \is_object($productId) && method_exists($productId, 'toString')
                ? $productId->toString()
                : (string) $productId;

            $summaries[$productId] = new ProductReviewRatingSummary(
                reviewCount: (int) $result['reviewCount'],
                ratingValue: (float) $result['ratingValue'],
            );
        }

        return $summaries;
    }

    public function createPublishedByProductQueryBuilder(
        Product $product,
        \DateTimeImmutable $now,
    ): QueryBuilder {
        return $this->createQueryBuilder('review')
            ->andWhere('review.product = :product')
            ->andWhere('review.status = :status')
            ->andWhere('review.publishedAt BETWEEN :publishedSince AND :publishedUntil')
            ->setParameter('product', $product)
            ->setParameter('status', ProductReview::STATUS_PUBLISHED)
            ->setParameter('publishedSince', $this->publicationPolicy->publicSince($now))
            ->setParameter('publishedUntil', $now);
    }

    /** @return list<ProductReview> */
    public function findPublishedByProduct(
        Product $product,
        ?\DateTimeImmutable $now = null,
    ): array {
        /** @var list<ProductReview> $reviews */
        $reviews = $this->createPublishedByProductQueryBuilder($product, $now ?? new \DateTimeImmutable())
            ->orderBy('review.publishedAt', 'DESC')
            ->getQuery()
            ->getResult();

        return $reviews;
    }

    /**
     * @param iterable<Product> $products
     *
     * @return list<ProductReview>
     */
    public function findPublishedByProducts(
        iterable $products,
        ?\DateTimeImmutable $now = null,
    ): array {
        $products = \is_array($products) ? array_values($products) : iterator_to_array($products, false);
        if ([] === $products) {
            return [];
        }

        $now ??= new \DateTimeImmutable();

        /** @var list<ProductReview> $reviews */
        $reviews = $this->createQueryBuilder('review')
            ->andWhere('review.product IN (:products)')
            ->andWhere('review.status = :status')
            ->andWhere('review.publishedAt BETWEEN :publishedSince AND :publishedUntil')
            ->setParameter('products', $products)
            ->setParameter('status', ProductReview::STATUS_PUBLISHED)
            ->setParameter('publishedSince', $this->publicationPolicy->publicSince($now))
            ->setParameter('publishedUntil', $now)
            ->orderBy('review.publishedAt', 'DESC')
            ->getQuery()
            ->getResult();

        return $reviews;
    }

    public function hasReview(ReviewInvitation $invitation, Product $product): bool
    {
        $review = $this->findForInvitationAndProduct($invitation, $product);

        return $review instanceof ProductReview
            && ProductReview::STATUS_REJECTED !== $review->getStatus();
    }

    public function findForInvitationAndProduct(
        ReviewInvitation $invitation,
        Product $product,
    ): ?ProductReview {
        $review = $this->findOneBy([
            'invitation' => $invitation,
            'product' => $product,
        ]);

        return $review instanceof ProductReview ? $review : null;
    }
}
