<?php

/*
 * 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\Cart\Repository;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\Promo;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Repository\BaseRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\User\UserInterface;

class CartRepository extends BaseRepository
{
    /**
     * @var array<string, array<int, array<string, mixed>>>
     */
    private array $stockSaleSinceStockAtCache = [];

    /**
     * CartRepository constructor.
     *
     * @throws \ReflectionException
     */
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Cart::class);
    }

    public function findOneCartInProgressById(string $uuid, bool $checkExpired = true): ?Cart
    {
        $qb = $this->createQueryBuilder('cart')
            ->leftJoin('cart.cartCheckout', 'checkout')
            ->addSelect('checkout')
            ->andWhere('cart.invoice is NULL')
            ->andWhere('(
                checkout.id IS NULL
                OR (
                    checkout.disabled = FALSE
                    AND checkout.payedAt IS NULL
                    AND checkout.orderedAt IS NULL
                )
            )')
            ->andWhere('cart.id = :id')
            ->setMaxResults(1)
            ->setParameter('id', $uuid);
        if ($checkExpired) {
            $date = new \DateTime();
            $qb->andWhere('cart.expiresAt > :date')
                ->setParameter('date', $date);
        }

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

    public function findOneCartInProgressByUser(UserInterface $user): ?Cart
    {
        return $this->findCartsInProgressByUser($user)[0] ?? null;
    }

    /**
     * @return list<Cart>
     */
    public function findCartsInProgressByUser(UserInterface $user, bool $checkExpired = true): array
    {
        $qb = $this->createQueryBuilder('cart')
            ->leftJoin('cart.cartCheckout', 'checkout')
            ->addSelect('checkout')
            ->andWhere('cart.invoice is NULL')
            ->andWhere('(
                checkout.id IS NULL
                OR (
                    checkout.disabled = FALSE
                    AND checkout.payedAt IS NULL
                    AND checkout.orderedAt IS NULL
                )
            )')
            ->andWhere('cart.user = :user')
            ->orderBy('cart.updatedAt', 'DESC')
            ->addOrderBy('cart.createdAt', 'DESC')
            ->setParameter('user', $user);

        if ($checkExpired) {
            $qb
                ->andWhere('cart.expiresAt > :date')
                ->setParameter('date', new \DateTime());
        }

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

    /**
     * @return array<int, Cart>
     */
    public function findTrackingAdmin(string $sort = 'DESC'): array
    {
        return $this->findTrackingAdminFiltered(sort: $sort);
    }

    /**
     * @return array<int, Cart>
     */
    public function findTrackingAdminFiltered(
        ?string $status = null,
        ?string $paymentMethodId = null,
        ?\DateTimeImmutable $createdFrom = null,
        ?\DateTimeImmutable $createdTo = null,
        string $sort = 'DESC',
    ): array
    {
        $qb = $this->createQueryBuilder('cart')
            ->leftJoin('cart.cartCheckout', 'checkout')
            ->addSelect('checkout')
            ->leftJoin('checkout.paymentMethod', 'payment_method')
            ->addSelect('payment_method')
            ->andWhere('EXISTS (
                SELECT 1
                FROM CpCreation\VitiCore\Cart\Model\CartItem item
                WHERE item.cart = cart
            )')
            ->orderBy('cart.createdAt', $sort)
            ->addOrderBy('cart.id', $sort);

        if ('paid' === $status) {
            $qb->andWhere('checkout.payedAt IS NOT NULL');
        } elseif ('pending' === $status) {
            $qb->andWhere('checkout.payedAt IS NULL');
        }

        if (null !== $paymentMethodId) {
            $qb
                ->andWhere('payment_method.id = :paymentMethodId')
                ->setParameter('paymentMethodId', $paymentMethodId);
        }

        if (null !== $createdFrom) {
            $qb
                ->andWhere('cart.createdAt >= :createdFrom')
                ->setParameter('createdFrom', $createdFrom);
        }

        if (null !== $createdTo) {
            $qb
                ->andWhere('cart.createdAt < :createdTo')
                ->setParameter('createdTo', $createdTo);
        }

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

    /**
     * @return list<Cart>
     */
    public function findPaidForStatsFiltered(
        ?string $paymentMethodId = null,
        ?\DateTimeImmutable $paidFrom = null,
        ?\DateTimeImmutable $paidTo = null,
    ): array
    {
        $qb = $this->createQueryBuilder('cart')
            ->innerJoin(
                'cart.cartCheckout',
                'checkout',
                Join::WITH,
                'checkout.disabled = FALSE AND checkout.payedAt IS NOT NULL'
            )
            ->addSelect('checkout')
            ->leftJoin('checkout.paymentMethod', 'payment_method')
            ->addSelect('payment_method')
            ->leftJoin('cart.items', 'items')
            ->addSelect('items')
            ->leftJoin('items.variant', 'variant')
            ->addSelect('variant')
            ->leftJoin('variant.product', 'product')
            ->addSelect('product')
            ->leftJoin('variant.type', 'type')
            ->addSelect('type')
            ->andWhere('EXISTS (
                SELECT 1
                FROM CpCreation\VitiCore\Cart\Model\CartItem item
                WHERE item.cart = cart
            )')
            ->orderBy('checkout.payedAt', 'ASC')
            ->addOrderBy('cart.createdAt', 'ASC');

        if (null !== $paymentMethodId) {
            $qb
                ->andWhere('payment_method.id = :paymentMethodId')
                ->setParameter('paymentMethodId', $paymentMethodId);
        }

        if (null !== $paidFrom) {
            $qb
                ->andWhere('checkout.payedAt >= :paidFrom')
                ->setParameter('paidFrom', $paidFrom);
        }

        if (null !== $paidTo) {
            $qb
                ->andWhere('checkout.payedAt < :paidTo')
                ->setParameter('paidTo', $paidTo);
        }

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

    public function findLastCartByUser(UserInterface $user): ?Cart
    {
        $qb = $this
            ->createQueryBuilder('cart')
            ->leftJoin('cart.cartCheckout', 'checkout')
            ->addSelect('checkout')
            ->andWhere('cart.invoice is NULL')
            ->andWhere('(
                checkout.id IS NULL
                OR (
                    checkout.disabled = FALSE
                    AND checkout.payedAt IS NULL
                    AND checkout.orderedAt IS NULL
                )
            )')
            ->andWhere('cart.user = :user')
            ->orderBy('cart.id', 'DESC')
            ->setMaxResults(1)
            ->setParameter('user', $user);

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

    /**
     * @return array<int, Cart>
     */
    public function findLastPaidByUser(UserInterface $user, int $limit = 5): array
    {
        return $this->findLastAccountOrdersByUser($user, $limit);
    }

    /**
     * Vue admin "Détail client" :
     * liste les commandes du client sur le même périmètre que le suivi
     * admin, y compris les commandes encore en cours.
     *
     * @return array<int, Cart>
     */
    public function findRecentAdminOrdersByUser(UserInterface $user, ?int $limit = 10): array
    {
        $qb = $this
            ->createQueryBuilder('cart')
            ->leftJoin('cart.cartCheckout', 'checkout')
            ->addSelect('checkout')
            ->leftJoin('checkout.paymentMethod', 'payment_method')
            ->addSelect('payment_method')
            ->leftJoin('cart.deliveryMethod', 'delivery_method')
            ->addSelect('delivery_method')
            ->andWhere('cart.user = :user')
            ->andWhere('EXISTS (
                SELECT 1
                FROM CpCreation\VitiCore\Cart\Model\CartItem item
                WHERE item.cart = cart
            )')
            ->orderBy('cart.createdAt', 'DESC')
            ->addOrderBy('cart.updatedAt', 'DESC')
            ->addOrderBy('checkout.createdAt', 'DESC')
            ->setParameter('user', $user);

        if (null !== $limit) {
            $qb->setMaxResults(max(1, $limit));
        }

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

    /**
     * @param list<UserInterface> $users
     *
     * @return list<string>
     */
    public function findUserIdsWithOrders(array $users): array
    {
        if ([] === $users) {
            return [];
        }

        $results = $this
            ->createQueryBuilder('cart')
            ->select('DISTINCT IDENTITY(cart.user) AS user_id')
            ->leftJoin('cart.cartCheckout', 'checkout')
            ->andWhere('cart.user IN (:users)')
            ->andWhere('EXISTS (
                SELECT 1
                FROM CpCreation\VitiCore\Cart\Model\CartItem item
                WHERE item.cart = cart
            )')
            ->andWhere('
                checkout.payedAt IS NOT NULL
                OR checkout.orderedAt IS NOT NULL
            ')
            ->setParameter('users', $users)
            ->getQuery()
            ->getScalarResult();

        return array_values(array_filter(array_map(
            static fn (array $row): ?string => isset($row['user_id']) && \is_string($row['user_id']) ? $row['user_id'] : null,
            $results
        )));
    }

    /**
     * Vue admin "Détail client" :
     * liste les commandes visibles dans le compte client, mais triées
     * par date de création décroissante.
     *
     * @return array<int, Cart>
     */
    public function findAccountOrdersByUserOrderByCreatedAt(UserInterface $user, ?int $limit = 10): array
    {
        $qb = $this
            ->createQueryBuilder('cart')
            ->innerJoin('cart.cartCheckout', 'checkout', Join::WITH, 'checkout.disabled = FALSE')
            ->addSelect('checkout')
            ->leftJoin('checkout.paymentMethod', 'payment_method')
            ->addSelect('payment_method')
            ->leftJoin('cart.deliveryMethod', 'delivery_method')
            ->addSelect('delivery_method')
            ->andWhere('cart.user = :user')
            ->andWhere('
                checkout.payedAt IS NOT NULL
                OR (
                    checkout.orderedAt IS NOT NULL
                    AND payment_method.paymentOnline = FALSE
                )
            ')
            ->orderBy('cart.createdAt', 'DESC')
            ->addOrderBy('checkout.createdAt', 'DESC')
            ->setParameter('user', $user);

        if (null !== $limit) {
            $qb->setMaxResults(max(1, $limit));
        }

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

    /**
     * Règle métier "Mon compte" avec les données actuellement persistées :
     * - paiement en ligne => uniquement les commandes payées ;
     * - paiement hors ligne => uniquement les commandes effectivement envoyées.
     * Tri : date de création décroissante.
     *
     * @return array<int, Cart>
     */
    public function findLastAccountOrdersByUser(UserInterface $user, ?int $limit = 5): array
    {
        $qb = $this
            ->createQueryBuilder('cart')
            ->innerJoin('cart.cartCheckout', 'checkout', Join::WITH, 'checkout.disabled = FALSE')
            ->addSelect('checkout')
            ->leftJoin('checkout.paymentMethod', 'payment_method')
            ->addSelect('payment_method')
            ->leftJoin('cart.deliveryMethod', 'delivery_method')
            ->addSelect('delivery_method')
            ->andWhere('cart.user = :user')
            ->andWhere('
                checkout.payedAt IS NOT NULL
                OR (
                    checkout.orderedAt IS NOT NULL
                    AND payment_method.paymentOnline = FALSE
                )
            ')
            ->orderBy('cart.createdAt', 'DESC')
            ->addOrderBy('checkout.createdAt', 'DESC')
            ->addOrderBy('checkout.payedAt', 'DESC')
            ->addOrderBy('checkout.orderedAt', 'DESC')
            ->addOrderBy('checkout.updatedAt', 'DESC')
            ->addOrderBy('cart.updatedAt', 'DESC')
            ->setParameter('user', $user);

        if (null !== $limit) {
            $qb->setMaxResults(max(1, $limit));
        }

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

    /**
     * @return array<int, Cart>
     */
    public function countPromoByUser(Promo $promo, Cart $cart): array
    {
        $phone = $cart->getPhone();
        $email = $cart->getEmail();

        if ((null === $phone || '' === $phone) && (null === $email || '' === $email)) {
            return [];
        }

        $qb = $this->createQueryBuilder('c');
        $qb
            ->andWhere('c.promoId = :promo')
            ->andWhere(
                $qb->expr()->orX(
                    'cart_checkout.payedAt IS NOT NULL',
                    'cart_checkout.orderedAt IS NOT NULL'
                )
            );

        $matches = $qb->expr()->orX();

        if (null !== $phone && '' !== $phone) {
            $matches->add(
                $qb->expr()->eq(
                    $qb->expr()->substring('invoice_address.phone', -8),
                    $qb->expr()->literal(mb_substr($phone, -8))
                )
            );
        }

        if (null !== $email && '' !== $email) {
            $matches->add(
                $qb->expr()->eq(
                    'invoice_address.email',
                    $qb->expr()->literal($email)
                )
            );
        }

        $qb->andWhere($matches);

        $qb
            ->innerJoin('c.cartCheckout', 'cart_checkout', Join::WITH, 'cart_checkout.cart = c.id')
            ->innerJoin('c.invoiceAddress', 'invoice_address')
            ->setParameter('promo', $promo->getId()->toString());

        $query = $qb->getQuery();

        return $query->getResult();
    }

    public function countAppliedPromoForCartContext(Promo $promo, Cart $cart): int
    {
        $candidates = $this->findPaidCartsForPromoContext($cart);
        $count = 0;

        foreach ($candidates as $candidate) {
            foreach ($candidate->getPromos() as $appliedPromo) {
                if (
                    $appliedPromo->promoId === $promo->getId()->toString()
                    || (
                        null !== $appliedPromo->code
                        && null !== $promo->getTitle()
                        && mb_strtoupper($appliedPromo->code) === mb_strtoupper($promo->getTitle())
                    )
                ) {
                    ++$count;
                    break;
                }
            }
        }

        return $count;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function stockSaleSinceStockAt(ProductVariant $variant): array
    {
        $cacheKey = \sprintf(
            '%s:%s',
            $variant->getId()->toString(),
            $variant->getStockAt()?->format(\DATE_ATOM) ?? 'null',
        );

        if (\array_key_exists($cacheKey, $this->stockSaleSinceStockAtCache)) {
            return $this->stockSaleSinceStockAtCache[$cacheKey];
        }

        $qb = $this->createQueryBuilder('c');

        $qb
            ->innerJoin('c.items', 'item', Join::WITH, 'item.variant = :variant')
            ->setParameter('variant', $variant->getId()->toString())
            ->innerJoin('c.cartCheckout', 'checkout')
            ->andWhere('checkout.payedAt IS NOT NULL')
            ->andWhere('c.createdAt > :stockAt')
            ->setParameter('stockAt', $variant->getStockAt())
            ->select('SUM(item.quantity) as quantitySale');

        $query = $qb->getQuery();

        return $this->stockSaleSinceStockAtCache[$cacheKey] = $query->execute();
    }

    /**
     * @return array<int, Cart>
     */
    private function findPaidCartsForPromoContext(Cart $cart): array
    {
        $qb = $this->createQueryBuilder('c');

        $qb
            ->innerJoin('c.cartCheckout', 'checkout', Join::WITH, 'checkout.disabled = FALSE')
            ->leftJoin('c.user', 'customer')
            ->leftJoin('c.invoiceAddress', 'invoice_address')
            ->leftJoin('c.deliveryAddress', 'delivery_address')
            ->andWhere(
                $qb->expr()->orX(
                    'checkout.payedAt IS NOT NULL',
                    'checkout.orderedAt IS NOT NULL'
                )
            );

        $matches = $qb->expr()->orX();
        $hasMatcher = false;

        if (null !== $cart->getUser()) {
            $matches->add('customer = :user');
            $qb->setParameter('user', $cart->getUser());
            $hasMatcher = true;
        }

        $emails = array_values(array_unique(array_filter([
            $cart->getInvoiceAddress()?->getEmail(),
            $cart->getDeliveryAddress()?->getEmail(),
        ], static fn (?string $value): bool => null !== $value && '' !== mb_trim($value))));

        if ([] !== $emails) {
            $matches->add($qb->expr()->in('invoice_address.email', ':emails'));
            $matches->add($qb->expr()->in('delivery_address.email', ':emails'));
            $qb->setParameter('emails', $emails);
            $hasMatcher = true;
        }

        $phones = array_values(array_unique(array_filter([
            $this->normalizePhoneSuffix($cart->getInvoiceAddress()?->getPhone()),
            $this->normalizePhoneSuffix($cart->getDeliveryAddress()?->getPhone()),
        ])));

        foreach ($phones as $index => $phone) {
            $parameter = 'phone_'.$index;
            $matches->add($qb->expr()->eq($qb->expr()->substring('invoice_address.phone', -8), ':'.$parameter));
            $matches->add($qb->expr()->eq($qb->expr()->substring('delivery_address.phone', -8), ':'.$parameter));
            $qb->setParameter($parameter, $phone);
            $hasMatcher = true;
        }

        if (!$hasMatcher) {
            return [];
        }

        $qb->andWhere($matches);

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

    private function normalizePhoneSuffix(?string $phone): ?string
    {
        if (null === $phone) {
            return null;
        }

        $digits = preg_replace('/\D+/', '', $phone);

        if (null === $digits || '' === $digits) {
            return null;
        }

        return mb_substr($digits, -8);
    }
}
