<?php

namespace Mautic\LeadBundle\Helper;

use Mautic\CoreBundle\Helper\ClickthroughHelper;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\IpLookupHelper;
use Mautic\EmailBundle\Entity\Stat;
use Mautic\EmailBundle\Entity\StatRepository;
use Mautic\EmailBundle\Helper\BotRatioHelper;
use Mautic\LeadBundle\DataObject\LeadManipulator;
use Mautic\LeadBundle\Deduplicate\ContactMerger;
use Mautic\LeadBundle\Deduplicate\Exception\SameContactException;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Event\ContactIdentificationEvent;
use Mautic\LeadBundle\Exception\ContactNotFoundException;
use Mautic\LeadBundle\LeadEvents;
use Mautic\LeadBundle\Model\LeadModel;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

class ContactRequestHelper
{
    /**
     * @var Lead|null
     */
    private $trackedContact;

    private array $queryFields = [];

    private array $publiclyUpdatableFieldValues = [];

    public function __construct(
        private LeadModel $leadModel,
        private ContactTracker $contactTracker,
        private CoreParametersHelper $coreParametersHelper,
        private IpLookupHelper $ipLookupHelper,
        private RequestStack $requestStack,
        private LoggerInterface $logger,
        private EventDispatcherInterface $eventDispatcher,
        private ContactMerger $contactMerger,
        private StatRepository $statRepository,
        private BotRatioHelper $botRatioHelper,
    ) {
    }

    public function getContactFromQuery(array $queryFields = []): ?Lead
    {
        $request = $this->getCurrentRequest();
        if ($request && $request->cookies->get('Blocked-Tracking')) {
            return null;
        }

        $ipAddress = $this->ipLookupHelper->getIpAddress();
        if (!$ipAddress->isTrackable()) {
            return null;
        }

        $dateTime  = new \DateTime();
        $userAgent = $request ? $request->server->get('HTTP_USER_AGENT') : '';
        if (!empty($queryFields['ct'])) {
            $queryFields['ct'] = (is_array($queryFields['ct'])) ? $queryFields['ct'] : ClickthroughHelper::decodeArrayFromUrl($queryFields['ct']);
        }

        if (isset($queryFields['ct']['stat'])) {
            /** @var Stat $stat */
            $stat = $this->statRepository->findOneBy(['trackingHash' => $queryFields['ct']['stat']]);
            if (null !== $stat && $this->botRatioHelper->isHitByBot($stat, $dateTime, $ipAddress, (string) $userAgent)) {
                return null;
            }
        }

        unset($queryFields['page_url']); // This is set now automatically by PageModel
        $this->queryFields    = $queryFields;

        try {
            $foundContact         = $this->getContactFromUrl();
            $this->trackedContact = $foundContact;
            $this->contactTracker->setTrackedContact($this->trackedContact);
        } catch (ContactNotFoundException) {
        }

        if (!$this->trackedContact) {
            $this->trackedContact = $this->contactTracker->getContact();
        }

        if (!$this->trackedContact) {
            return null;
        }

        $this->prepareContactFromRequest();

        return $this->trackedContact;
    }

    /**
     * @throws ContactNotFoundException
     */
    private function getContactFromUrl(): Lead
    {
        $request = $this->getCurrentRequest();

        if ($request && $request->cookies->get('Blocked-Tracking')) {
            throw new ContactNotFoundException();
        }

        // Check for a lead requested through clickthrough query parameter
        if (isset($this->queryFields['ct'])) {
            $clickthrough = (is_array($this->queryFields['ct'])) ? $this->queryFields['ct'] : ClickthroughHelper::decodeArrayFromUrl($this->queryFields['ct']);
        } elseif ($request && $clickthrough = $request->get('ct', [])) {
            $clickthrough = ClickthroughHelper::decodeArrayFromUrl($clickthrough);
        }

        if (!is_array($clickthrough)) {
            throw new ContactNotFoundException();
        }

        try {
            return $this->getContactFromClickthrough($clickthrough);
        } catch (ContactNotFoundException) {
        }

        $this->setEmailFromClickthroughIdentification($clickthrough);

        /* @var Lead $foundContact */
        if (!empty($this->queryFields)) {
            [$foundContact, $this->publiclyUpdatableFieldValues] = $this->leadModel->checkForDuplicateContact(
                $this->queryFields,
                true,
                true
            );

            if ($this->trackedContact && $this->trackedContact->getId() && $foundContact->getId()) {
                try {
                    $foundContact = $this->contactMerger->merge($this->trackedContact, $foundContact);
                } catch (SameContactException) {
                }
            }

            if (is_null($this->trackedContact) or $foundContact->getId() !== $this->trackedContact->getId()) {
                // A contact was found by a publicly updatable field
                if (!$foundContact->isNew()) {
                    return $foundContact;
                }
            }
        }

        throw new ContactNotFoundException();
    }

    /**
     * Identify a contact through a clickthrough URL.
     *
     * @return Lead
     *
     * @throws ContactNotFoundException
     */
    private function getContactFromClickthrough(array $clickthrough)
    {
        $event = new ContactIdentificationEvent($clickthrough);
        $this->eventDispatcher->dispatch($event, LeadEvents::ON_CLICKTHROUGH_IDENTIFICATION);

        if ($contact = $event->getIdentifiedContact()) {
            $this->logger->debug("LEAD: Contact ID# {$contact->getId()} tracked through clickthrough query by the ".$event->getIdentifier().' channel');

            // Merge tracked visitor into the clickthrough contact
            return $this->mergeWithTrackedContact($contact);
        }

        throw new ContactNotFoundException();
    }

    private function setEmailFromClickthroughIdentification(array $clickthrough): void
    {
        if (!$this->coreParametersHelper->get('track_by_tracking_url') || !empty($queryFields['email'])) {
            return;
        }

        if (empty($clickthrough['lead']) || !$foundContact = $this->leadModel->getEntity($clickthrough['lead'])) {
            return;
        }

        // Identify contact from link if email field is set as publicly updateable
        if ($email = $foundContact->getEmail()) {
            // Add email to query for checkForDuplicateContact to pick up and merge
            $this->queryFields['email'] = $email;
            $this->logger->debug("LEAD: Contact ID# {$clickthrough['lead']} tracked through clickthrough query.");

            return;
        }
    }

    private function prepareContactFromRequest(): void
    {
        $ipAddress          = $this->ipLookupHelper->getIpAddress();
        $contactIpAddresses = $this->trackedContact->getIpAddresses();
        if (!$contactIpAddresses->contains($ipAddress)) {
            $this->trackedContact->addIpAddress($ipAddress);
        }

        if (!empty($this->publiclyUpdatableFieldValues)) {
            $this->leadModel->setFieldValues(
                $this->trackedContact,
                $this->publiclyUpdatableFieldValues,
                false,
                true,
                true
            );
        }

        // Assume a web request as this is likely a tracking request from DWC or tracking code
        $this->trackedContact->setManipulator(
            new LeadManipulator(
                'page',
                'hit',
                null,
                $this->queryFields['page_url'] ?? ''
            )
        );

        if (isset($this->queryFields['tags'])) {
            $this->leadModel->modifyTags($this->trackedContact, $this->queryFields['tags']);
        }
    }

    /**
     * @return Lead
     */
    private function mergeWithTrackedContact(Lead $foundContact)
    {
        if ($this->trackedContact && $this->trackedContact->getId() && $this->trackedContact->isAnonymous()) {
            try {
                return $this->contactMerger->merge($this->trackedContact, $foundContact);
            } catch (SameContactException) {
            }
        }

        return $foundContact;
    }

    private function getCurrentRequest(): ?Request
    {
        return $this->requestStack->getCurrentRequest();
    }
}
