<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\CartItem;
use CpCreation\VitiCore\Cart\Model\PaymentMethod;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Messenger\MessageBusInterface;

final readonly class MetaConversionsApiTracker
{
    public function __construct(
        private ShopConfigRepository $shopConfigRepository,
        private MarketingAttributionStorage $marketingAttributionStorage,
        private MessageBusInterface $messageBus,
        private ?LoggerInterface $logger = null,
    ) {
    }

    public function trackPurchase(Cart $cart): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $pricing = $cart->getPricing();

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

        $event = [
            'event_name' => 'Purchase',
            'event_time' => time(),
            'event_id' => $this->buildPurchaseEventId($cart),
            'action_source' => 'website',
            'event_source_url' => $this->resolveEventSourceUrl($cart),
            'user_data' => $this->buildUserData($cart),
            'custom_data' => [
                'currency' => 'EUR',
                'value' => round($pricing->totalTTC / 100, 2),
                'order_id' => $cart->getToken(),
                'content_type' => 'product',
                'content_ids' => $this->buildContentIds($cart),
                'content_name' => $this->buildContentName($cart),
                'product_names' => $this->buildContentNames($cart),
                'contents' => $this->buildContents($cart),
                'num_items' => $this->countItems($cart),
            ],
        ];

        $payload = [
            'data' => [$event],
        ];

        $this->sendEventToConfiguredPixels($payload, ['cart_id' => $cart->getId()->toString()]);
    }

    public function trackInitiateCheckout(Cart $cart, ?Request $request = null): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $pricing = $cart->getPricing();
        $customData = [
            'currency' => 'EUR',
            'content_type' => 'product',
            'content_ids' => $this->buildContentIds($cart),
            'content_name' => $this->buildContentName($cart),
            'product_names' => $this->buildContentNames($cart),
            'contents' => $this->buildContents($cart),
            'num_items' => $this->countItems($cart),
        ];

        if (null !== $pricing) {
            $customData['value'] = round($pricing->totalTTC / 100, 2);
        }

        $event = [
            'event_name' => 'InitiateCheckout',
            'event_time' => time(),
            'event_id' => $this->buildInitiateCheckoutEventId($cart),
            'action_source' => 'website',
            'event_source_url' => $request?->getUri() ?? $this->resolveEventSourceUrl($cart),
            'user_data' => $this->buildUserData($cart, $request),
            'custom_data' => $customData,
        ];

        $payload = [
            'data' => [$event],
        ];

        $this->sendEventToConfiguredPixels($payload, [
            'event_name' => 'InitiateCheckout',
            'event_id' => $event['event_id'],
            'cart_id' => $cart->getId()->toString(),
        ]);
    }

    public function trackAddToCart(CartItem $item, ?Request $request = null): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $variant = $item->getVariant();
        $variantId = $variant?->getId()?->toString();

        if (null === $variant || null === $variantId) {
            return;
        }

        $cart = $item->getCart();
        $event = [
            'event_name' => 'AddToCart',
            'event_time' => time(),
            'event_id' => $this->buildAddToCartEventId($item),
            'action_source' => 'website',
            'event_source_url' => $request?->getUri() ?? ($cart instanceof Cart ? $this->resolveEventSourceUrl($cart) : null),
            'user_data' => $cart instanceof Cart
                ? $this->buildUserData($cart, $request)
                : $this->buildRequestUserData($request, $item->getId()->toString()),
            'custom_data' => [
                'currency' => 'EUR',
                'value' => round($item->getTotalFinalTTC() / 100, 2),
                'content_ids' => [$variantId],
                'content_name' => $variant->getFullName(),
                'content_type' => 'product',
                'contents' => [[
                    'id' => $variantId,
                    'quantity' => $item->getQuantity(),
                    'item_price' => round($item->getFinalPriceTTC() / 100, 2),
                ]],
                'num_items' => $item->getQuantity(),
            ],
        ];

        $payload = [
            'data' => [$event],
        ];

        $this->sendEventToConfiguredPixels($payload, [
            'event_name' => 'AddToCart',
            'event_id' => $event['event_id'],
            'cart_id' => $cart instanceof Cart ? $cart->getId()->toString() : null,
            'cart_item_id' => $item->getId()->toString(),
        ]);
    }

    public function trackAddPaymentInfo(Cart $cart, ?Request $request = null): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $pricing = $cart->getPricing();
        $paymentMethod = $cart->getPaymentMethod();
        $checkout = $cart->getCartCheckout();

        if (null === $pricing || null === $paymentMethod || null === $checkout) {
            return;
        }

        $paymentType = $paymentMethod->isCheckoutOnlineMethod() ? 'online' : 'offline';
        $event = [
            'event_name' => 'AddPaymentInfo',
            'event_time' => time(),
            'event_id' => $this->buildAddPaymentInfoEventId($cart),
            'action_source' => 'website',
            'event_source_url' => $request?->getUri() ?? $this->resolveEventSourceUrl($cart),
            'user_data' => $this->buildUserData($cart, $request),
            'custom_data' => [
                'currency' => 'EUR',
                'value' => round($pricing->totalTTC / 100, 2),
                'content_type' => 'product',
                'content_ids' => $this->buildContentIds($cart),
                'content_name' => $this->buildContentName($cart),
                'product_names' => $this->buildContentNames($cart),
                'contents' => $this->buildContents($cart),
                'num_items' => $this->countItems($cart),
                'payment_type' => $paymentType,
                'payment_method' => $this->resolvePaymentMethodLabel($paymentMethod),
            ],
        ];

        $payload = [
            'data' => [$event],
        ];

        $this->sendEventToConfiguredPixels($payload, [
            'event_name' => 'AddPaymentInfo',
            'event_id' => $event['event_id'],
            'cart_id' => $cart->getId()->toString(),
        ]);
    }

    public function trackAbandonedCart(Cart $cart): void
    {
        $this->trackCartLifecycleEvent($cart, 'AbandonedCart');
    }

    public function trackReactivatedCart(Cart $cart, ?Request $request = null): void
    {
        $this->trackCartLifecycleEvent($cart, 'ReactivatedCart', $request);
    }

    public function trackPaymentFailed(Cart $cart, ?Request $request = null): void
    {
        $this->trackCartLifecycleEvent($cart, 'PaymentFailed', $request);
    }

    public function trackPaymentCanceled(Cart $cart, ?Request $request = null): void
    {
        $this->trackCartLifecycleEvent($cart, 'PaymentCanceled', $request);
    }

    public function trackCartCleared(Cart $cart, ?Request $request = null): void
    {
        $this->trackCartLifecycleEvent($cart, 'CartCleared', $request);
    }

    public function trackViewContent(Product $product, mixed $priceGroupId = null, ?Request $request = null): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $variant = $product->getVariantByDefault();

        if (!$variant instanceof ProductVariant) {
            return;
        }

        $variantId = $variant->getId()->toString();
        $price = null !== $priceGroupId ? $variant->getPriceByGroupId($priceGroupId) : null;
        $customData = [
            'content_ids' => [$variantId],
            'content_name' => $product->getFullName(),
            'content_type' => 'product',
            'contents' => [[
                'id' => $variantId,
                'quantity' => 1,
                'item_price' => null !== $price ? round($price->getFinalPrice() / 100, 2) : 0.0,
            ]],
            'currency' => 'EUR',
            'value' => null !== $price ? round($price->getFinalPrice() / 100, 2) : 0.0,
        ];

        $event = [
            'event_name' => 'ViewContent',
            'event_time' => time(),
            'event_id' => $this->buildViewContentEventId($product),
            'action_source' => 'website',
            'event_source_url' => $request?->getUri(),
            'user_data' => $this->buildRequestUserData($request, $variantId),
            'custom_data' => $customData,
        ];

        $payload = [
            'data' => [$event],
        ];

        $this->sendEventToConfiguredPixels($payload, [
            'event_name' => 'ViewContent',
            'event_id' => $event['event_id'],
            'product_id' => $product->getId()->toString(),
        ]);
    }

    /**
     * @param array<string, mixed> $contactData
     */
    public function trackLead(array $contactData, ?Request $request = null): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $eventId = 'lead_'.bin2hex(random_bytes(16));
        $event = [
            'event_name' => 'Lead',
            'event_time' => time(),
            'event_id' => $eventId,
            'action_source' => 'website',
            'event_source_url' => $request?->getUri(),
            'user_data' => $this->buildLeadUserData($contactData, $request),
            'custom_data' => [
                'content_name' => 'Contact',
            ],
        ];

        $payload = [
            'data' => [$event],
        ];

        $this->sendEventToConfiguredPixels($payload, [
            'event_name' => 'Lead',
            'event_id' => $eventId,
        ]);
    }

    public function buildPurchaseEventId(Cart $cart): string
    {
        return 'purchase_'.$cart->getId()->toString();
    }

    public function buildInitiateCheckoutEventId(Cart $cart): string
    {
        return 'initiate_checkout_'.$cart->getId()->toString();
    }

    public function buildAddToCartEventId(CartItem $item): string
    {
        return 'add_to_cart_'.$item->getId()->toString();
    }

    public function buildAddPaymentInfoEventId(Cart $cart): string
    {
        $checkout = $cart->getCartCheckout();
        $paymentMethod = $cart->getPaymentMethod();

        return 'add_payment_info_'
            .($checkout?->getId()?->toString() ?? $cart->getId()->toString())
            .'_'
            .($paymentMethod?->getId()?->toString() ?? 'unknown');
    }

    public function buildAbandonedCartEventId(Cart $cart): string
    {
        return 'abandoned_cart_'
            .$cart->getId()->toString()
            .'_'
            .($cart->getAbandonedAt()?->getTimestamp() ?? time());
    }

    public function buildReactivatedCartEventId(Cart $cart): string
    {
        return 'reactivated_cart_'
            .$cart->getId()->toString()
            .'_'
            .($cart->getLastActivityAt()?->getTimestamp() ?? time());
    }

    public function buildPaymentReturnEventId(Cart $cart, string $eventName): string
    {
        $response = $cart->getCartCheckout()?->getResponse() ?? [];
        $capturedAt = \is_array($response)
            ? ($response['payment_return']['captured_at'] ?? null)
            : null;

        return mb_strtolower((string) preg_replace('/(?<!^)[A-Z]/', '_$0', $eventName))
            .'_'
            .$cart->getId()->toString()
            .'_'
            .(\is_scalar($capturedAt) ? hash('crc32b', (string) $capturedAt) : time());
    }

    public function buildCartClearedEventId(Cart $cart): string
    {
        return 'cart_cleared_'.$cart->getId()->toString().'_'.time();
    }

    public function buildViewContentEventId(Product $product): string
    {
        return 'view_content_'.$product->getId()->toString().'_'.bin2hex(random_bytes(8));
    }

    /**
     * @param array<string, mixed> $payload
     * @param array<string, mixed> $context
     */
    private function sendEventToConfiguredPixels(array $payload, array $context = []): void
    {
        try {
            $this->messageBus->dispatch(new ServerTrackingMessage(
                provider: ServerTrackingMessage::PROVIDER_META,
                payload: $payload,
                context: $context,
            ));
        } catch (\Throwable $exception) {
            $this->logger?->warning('Meta Conversions API tracking enqueue failed.', [
                'exception' => $exception,
            ] + $context);
        }
    }

    private function trackCartLifecycleEvent(Cart $cart, string $eventName, ?Request $request = null): void
    {
        $config = $this->shopConfigRepository->findOne();

        if (null === $config || !$config->isMetaConversionsApiConfigured()) {
            return;
        }

        $pricing = $cart->getPricing();
        $customData = [
            'currency' => 'EUR',
            'content_type' => 'product',
            'content_ids' => $this->buildContentIds($cart),
            'content_name' => $this->buildContentName($cart),
            'product_names' => $this->buildContentNames($cart),
            'contents' => $this->buildContents($cart),
            'num_items' => $this->countItems($cart),
            'checkout_status' => $cart->getCheckoutStatus(),
        ];

        if (null !== $pricing) {
            $customData['value'] = round($pricing->totalTTC / 100, 2);
        }

        $eventId = match ($eventName) {
            'AbandonedCart' => $this->buildAbandonedCartEventId($cart),
            'ReactivatedCart' => $this->buildReactivatedCartEventId($cart),
            'PaymentFailed', 'PaymentCanceled' => $this->buildPaymentReturnEventId($cart, $eventName),
            'CartCleared' => $this->buildCartClearedEventId($cart),
            default => mb_strtolower($eventName).'_'.$cart->getId()->toString().'_'.time(),
        };

        $event = [
            'event_name' => $eventName,
            'event_time' => time(),
            'event_id' => $eventId,
            'action_source' => 'website',
            'event_source_url' => $request?->getUri() ?? $this->resolveEventSourceUrl($cart),
            'user_data' => $this->buildUserData($cart, $request),
            'custom_data' => $customData,
        ];

        $this->sendEventToConfiguredPixels(['data' => [$event]], [
            'event_name' => $eventName,
            'event_id' => $eventId,
            'cart_id' => $cart->getId()->toString(),
        ]);
    }

    /**
     * @return array<string, mixed>
     */
    private function buildUserData(Cart $cart, ?Request $request = null): array
    {
        $checkout = $cart->getCartCheckout();
        $attribution = $checkout?->getMarketingAttribution() ?? $cart->getMarketingAttribution() ?? [];
        $matomo = \is_array($attribution['matomo'] ?? null) ? $attribution['matomo'] : [];
        $metaUserData = $this->buildMetaBrowserUserData($request, $attribution);

        $userData = array_filter([
            'client_ip_address' => $cart->getIpClient() ?? $request?->getClientIp(),
            'client_user_agent' => (\is_string($matomo['user_agent'] ?? null) ? $matomo['user_agent'] : null)
                ?? $request?->headers->get('User-Agent'),
            'em' => $this->hashNullable($cart->getEmail(), normalizeEmail: true),
            'ph' => $this->hashNullable($this->normalizePhone($cart->getPhone())),
            'fn' => $this->hashNullable($cart->getFirstname()),
            'ln' => $this->hashNullable($cart->getLastname()),
            'ct' => $this->hashNullable($cart->getCity()),
            'zp' => $this->hashNullable($cart->getPostalCode()),
            'country' => $this->hashNullable($cart->getCountry()),
            'external_id' => hash('sha256', $cart->getId()->toString()),
        ], static fn (mixed $value): bool => null !== $value && '' !== $value);

        return $metaUserData + $userData;
    }

    /**
     * @return array<string, mixed>
     */
    private function buildRequestUserData(?Request $request, ?string $externalId = null): array
    {
        $metaUserData = $this->buildMetaBrowserUserData($request);

        $userData = array_filter([
            'client_ip_address' => $request?->getClientIp(),
            'client_user_agent' => $request?->headers->get('User-Agent'),
            'external_id' => null !== $externalId ? hash('sha256', $externalId) : null,
        ], static fn (mixed $value): bool => null !== $value && '' !== $value);

        return $metaUserData + $userData;
    }

    /**
     * @param array<string, mixed> $contactData
     *
     * @return array<string, mixed>
     */
    private function buildLeadUserData(array $contactData, ?Request $request): array
    {
        $metaUserData = $this->buildMetaBrowserUserData($request);

        $userData = array_filter([
            'client_ip_address' => $request?->getClientIp(),
            'client_user_agent' => $request?->headers->get('User-Agent'),
            'em' => $this->hashNullable($this->stringValue($contactData['email'] ?? null), normalizeEmail: true),
            'ph' => $this->hashNullable($this->normalizePhone($this->stringValue($contactData['phone'] ?? null))),
            'fn' => $this->hashNullable($this->stringValue($contactData['firstname'] ?? null)),
            'ln' => $this->hashNullable($this->stringValue($contactData['lastname'] ?? null)),
            'external_id' => $this->hashNullable($this->stringValue($contactData['email'] ?? null), normalizeEmail: true),
        ], static fn (mixed $value): bool => null !== $value && '' !== $value);

        return $metaUserData + $userData;
    }

    /**
     * @param array<string, mixed> $attribution
     *
     * @return array{fbp?: string, fbc?: string}
     */
    private function buildMetaBrowserUserData(?Request $request, array $attribution = []): array
    {
        $storedAttribution = $this->marketingAttributionStorage->getCurrent($request) ?? [];
        $meta = array_replace(
            \is_array($storedAttribution['meta'] ?? null) ? $storedAttribution['meta'] : [],
            \is_array($attribution['meta'] ?? null) ? $attribution['meta'] : [],
        );

        $fbp = \is_string($meta['fbp'] ?? null) ? $meta['fbp'] : $this->stringValue($request?->cookies->get('_fbp'));
        $fbc = \is_string($meta['fbc'] ?? null) ? $meta['fbc'] : $this->stringValue($request?->cookies->get('_fbc'));
        $fbclid = $this->stringValue($request?->query->get('fbclid'));

        if (null === $fbc && null !== $fbclid) {
            $fbc = sprintf('fb.1.%d.%s', (int) floor(microtime(true) * 1000), $fbclid);
        }

        return array_filter([
            'fbp' => $fbp,
            'fbc' => $fbc,
        ], static fn (mixed $value): bool => null !== $value && '' !== $value);
    }

    private function resolveEventSourceUrl(Cart $cart): ?string
    {
        $checkout = $cart->getCartCheckout();
        $attribution = $checkout?->getMarketingAttribution() ?? $cart->getMarketingAttribution();
        $matomo = \is_array($attribution['matomo'] ?? null) ? $attribution['matomo'] : [];

        if (\is_string($matomo['url'] ?? null)) {
            return $matomo['url'];
        }

        return \is_array($attribution) && \is_string($attribution['landing_url'] ?? null)
            ? $attribution['landing_url']
            : null;
    }

    /**
     * @return list<string>
     */
    private function buildContentIds(Cart $cart): array
    {
        $contentIds = [];

        foreach ($cart->getItems() as $item) {
            $variant = $item->getVariant();
            $id = $variant?->getId()?->toString();

            if (null !== $id && !\in_array($id, $contentIds, true)) {
                $contentIds[] = $id;
            }
        }

        return $contentIds;
    }

    /**
     * @return list<string>
     */
    private function buildContentNames(Cart $cart): array
    {
        $contentNames = [];

        foreach ($cart->getItems() as $item) {
            $name = $item->getVariant()?->getFullName();

            if (null !== $name && '' !== $name && !\in_array($name, $contentNames, true)) {
                $contentNames[] = $name;
            }
        }

        return $contentNames;
    }

    private function buildContentName(Cart $cart): ?string
    {
        $contentNames = $this->buildContentNames($cart);

        return [] !== $contentNames ? implode(', ', $contentNames) : null;
    }

    /**
     * @return list<array{id: string, quantity: int, item_price: float}>
     */
    private function buildContents(Cart $cart): array
    {
        $contents = [];

        foreach ($cart->getItems() as $item) {
            $variant = $item->getVariant();
            $id = $variant?->getId()?->toString();

            if (null === $id || $item->getQuantity() <= 0) {
                continue;
            }

            $contents[] = [
                'id' => $id,
                'quantity' => $item->getQuantity(),
                'item_price' => round($item->getFinalPriceTTC() / 100, 2),
            ];
        }

        return $contents;
    }

    private function countItems(Cart $cart): int
    {
        $quantity = 0;

        foreach ($cart->getItems() as $item) {
            $quantity += $item->getQuantity();
        }

        return $quantity;
    }

    private function resolvePaymentMethodLabel(PaymentMethod $paymentMethod): ?string
    {
        $configuration = method_exists($paymentMethod, 'getConfiguration')
            ? $paymentMethod->getConfiguration()
            : [];

        $configuredName = $configuration['name'] ?? null;
        if (\is_string($configuredName) && '' !== $configuredName) {
            return $configuredName;
        }

        return $paymentMethod->getMethod() ?? ($paymentMethod->isCheckoutOnlineMethod() ? 'online' : 'offline');
    }

    private function hashNullable(?string $value, bool $normalizeEmail = false): ?string
    {
        $value = null !== $value ? mb_strtolower(mb_trim($value)) : null;

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

        if ($normalizeEmail) {
            $value = preg_replace('/\s+/', '', $value) ?? $value;
        }

        return hash('sha256', $value);
    }

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

        $phone = preg_replace('/[^0-9+]/', '', $phone) ?? '';

        return '' !== $phone ? $phone : null;
    }

    private function stringValue(mixed $value): ?string
    {
        return \is_scalar($value) ? (string) $value : null;
    }
}
