<?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\Bridge\Doctrine\EventSubscriber;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\CartCheckout;
use CpCreation\VitiCore\Cart\Model\Invoice;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking\MarketingAttributionStorage;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking\MetaConversionsApiTracker;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking\MatomoEcommerceTracker;
use CpCreation\VitiCore\Cart\Repository\InvoiceRepository;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

#[AsDoctrineListener(event: Events::prePersist)]
#[AsDoctrineListener(event: Events::onFlush)]
#[AsDoctrineListener(event: Events::postFlush)]
class CartCheckoutSubscriber
{
    /**
     * @var array<string, Cart>
     */
    private array $paidCartsToTrack = [];

    /**
     * @var array<string, Cart>
     */
    private array $paidCartsToTrackWithMeta = [];

    public function __construct(
        private readonly RequestStack $requestStack,
        private readonly ShopConfigRepository $shopConfigRepository,
        private readonly InvoiceRepository $invoiceRepository,
        private readonly MarketingAttributionStorage $marketingAttributionStorage,
        private readonly MatomoEcommerceTracker $matomoEcommerceTracker,
        private readonly MetaConversionsApiTracker $metaConversionsApiTracker,
    ) {
    }

    public function prePersist(LifecycleEventArgs $event): void
    {
        $entity = $event->getObject();

        if (!$entity instanceof CartCheckout) {
            return;
        }

        $cart = $entity->getCart();

        if (null === $cart) {
            return;
        }

        $request = $this->requestStack->getCurrentRequest() ?? $this->requestStack->getMainRequest();

        if (null === $request) {
            return;
        }

        $cart->setLocale($request->getLocale());
        $cart->setIpClient($request->getClientIp());
        $this->captureMatomoContext($entity);
    }

    public function onFlush(OnFlushEventArgs $eventArgs): void
    {
        $em = $eventArgs->getObjectManager();
        $uow = $em->getUnitOfWork();

        $nextDebitNumber = null;

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            if (!$entity instanceof CartCheckout) {
                continue;
            }

            $changes = $uow->getEntityChangeSet($entity);

            // payedAt vient d'être renseigné
            if (isset($changes['payedAt']) && null !== $changes['payedAt'][1]) {
                $cart = $entity->getCart();
                $this->captureMatomoContext($entity);
                if (null === $entity->getOrderedAt()) {
                    $entity->setOrderedAt(\DateTimeImmutable::createFromInterface($entity->getPayedAt()));
                }
                $uow->recomputeSingleEntityChangeSet($em->getClassMetadata($entity::class), $entity);

                $shopConfig = $this->shopConfigRepository->findOne();
                if ($shopConfig?->isInvoice()) {
                    $cart->promoteToInvoiceDocument();
                    $uow->recomputeSingleEntityChangeSet($em->getClassMetadata(Cart::class), $cart);
                }

                if ($shopConfig?->isInvoice() && null === $cart->getInvoice()) {
                    $nextDebitNumber ??= $this->invoiceRepository->getNextDebitNumber();
                    $this->createInvoiceForCart($cart, $nextDebitNumber, $em, $uow);
                    ++$nextDebitNumber;
                }

                if ($shopConfig?->isMatomoConfigured() && $shopConfig->isEnableMatomoEcommerceTracking()) {
                    $this->paidCartsToTrack[$cart->getId()->toString()] = $cart;
                }

                if ($shopConfig?->isMetaConversionsApiConfigured()) {
                    $this->paidCartsToTrackWithMeta[$cart->getId()->toString()] = $cart;
                }
            }
        }
    }

    public function postFlush(PostFlushEventArgs $eventArgs): void
    {
        $carts = $this->paidCartsToTrack;
        $this->paidCartsToTrack = [];

        foreach ($carts as $cart) {
            $this->matomoEcommerceTracker->trackOrder($cart);
        }

        if ([] === $this->paidCartsToTrackWithMeta) {
            return;
        }

        $metaCarts = $this->paidCartsToTrackWithMeta;
        $this->paidCartsToTrackWithMeta = [];

        foreach ($metaCarts as $cart) {
            $this->metaConversionsApiTracker->trackPurchase($cart);
        }
    }

    private function createInvoiceForCart(Cart $cart, int $numberDebit, EntityManagerInterface $em, UnitOfWork $uow): void
    {
        $invoice = new Invoice('shop');
        $invoice->setNumberDebit($numberDebit);
        $cart->setInvoice($invoice);

        $em->persist($invoice);
        $uow->computeChangeSet($em->getClassMetadata(Invoice::class), $invoice);
        $uow->recomputeSingleEntityChangeSet($em->getClassMetadata(Cart::class), $cart);
    }

    private function captureMatomoContext(CartCheckout $checkout): void
    {
        $request = $this->requestStack->getCurrentRequest() ?? $this->requestStack->getMainRequest();

        if (null === $request) {
            return;
        }

        $config = $this->shopConfigRepository->findOne();
        $siteId = $config?->getMatomoSiteId();
        $isAdminRequest = $this->isAdminRequest($request);
        $attribution = $checkout->getMarketingAttribution()
            ?? $checkout->getCart()?->getMarketingAttribution()
            ?? (!$isAdminRequest ? $this->marketingAttributionStorage->getCurrent($request) : null)
            ?? [];

        $matomo = \is_array($attribution['matomo'] ?? null) ? $attribution['matomo'] : [];

        if (!$isAdminRequest) {
            $matomo += array_filter([
                'visitor_id' => $this->resolveMatomoVisitorId($request, $siteId)
                    ?? (\is_string($attribution['visitor_id'] ?? null) ? $attribution['visitor_id'] : null),
                'user_agent' => $this->trimNullable($request->headers->get('User-Agent'), 512),
                'url' => $this->trimNullable($request->getUri(), 2000),
                'referrer' => $this->trimNullable($request->headers->get('Referer'), 2000),
            ], static fn (mixed $value): bool => null !== $value && '' !== $value);
        } elseif (!isset($matomo['visitor_id']) && \is_string($attribution['visitor_id'] ?? null)) {
            $matomo['visitor_id'] = $attribution['visitor_id'];
        }

        if ([] !== $matomo) {
            $attribution['matomo'] = $matomo;
        }

        if ([] !== $attribution) {
            $checkout->setMarketingAttribution($attribution);
        }
    }

    private function resolveMatomoVisitorId(Request $request, ?int $siteId): ?string
    {
        if (null === $siteId) {
            return null;
        }

        foreach ($request->cookies->all() as $name => $value) {
            if (!\is_string($value) || !str_starts_with($name, '_pk_id.'.$siteId.'.')) {
                continue;
            }

            $visitorId = strtok($value, '.');

            return \is_string($visitorId) && preg_match('/^[a-f0-9]{16}$/i', $visitorId)
                ? mb_strtolower($visitorId)
                : null;
        }

        return null;
    }

    private function isAdminRequest(Request $request): bool
    {
        $route = $request->attributes->get('_route');

        return \is_string($route) && str_starts_with($route, 'admin_');
    }

    private function trimNullable(?string $value, int $length): ?string
    {
        $value = null !== $value ? mb_trim($value) : null;

        return '' !== (string) $value ? mb_substr((string) $value, 0, $length) : null;
    }
}
