<?php

namespace Mautic\LeadBundle\Model;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\PersistentCollection;
use Mautic\CacheBundle\Cache\CacheProvider;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Model\MauticModelInterface;
use Mautic\LeadBundle\Entity\DoNotContact as DNC;
use Mautic\LeadBundle\Entity\DoNotContactRepository;
use Mautic\LeadBundle\Entity\Lead;

class DoNotContact implements MauticModelInterface
{
    public function __construct(
        protected LeadModel $leadModel,
        protected DoNotContactRepository $dncRepo,
        protected CoreParametersHelper $coreParametersHelper,
        protected CacheProvider $cacheProvider,
    ) {
    }

    /**
     * Remove a Lead's DNC entry based on channel.
     *
     * @param int|Lead  $contact
     * @param string    $channel
     * @param bool|true $persist
     * @param int|null  $reason
     */
    public function removeDncForContact($contact, $channel, $persist = true, $reason = null): bool
    {
        if (is_numeric($contact)) {
            $contact = $this->leadModel->getEntity($contact);
        }

        if (null === $contact) {
            return false;
        }

        /** @var DNC $dnc */
        foreach ($contact->getDoNotContact() as $dnc) {
            if ($dnc->getChannel() === $channel) {
                // Skip if reason doesn't match
                // Some integrations (Sugar CRM) can use both reasons (unsubscribed, bounced)
                if ($reason && $dnc->getReason() != $reason) {
                    continue;
                }
                $contact->removeDoNotContactEntry($dnc);

                if ($persist) {
                    $this->leadModel->saveEntity($contact);
                }

                return true;
            }
        }

        return false;
    }

    /**
     * Create a DNC entry for a lead.
     *
     * @param Lead|int|null  $contact
     * @param string|mixed[] $channel                  If an array with an ID, use the structure ['email' => 123]
     * @param string         $comments
     * @param int            $reason                   Must be a class constant from the DoNotContact class
     * @param bool           $persist
     * @param bool           $checkCurrentStatus
     * @param bool           $allowUnsubscribeOverride
     *
     * @return bool|DNC If a DNC entry is added or updated, returns the DoNotContact object. If a DNC is already present
     *                  and has the specified reason, nothing is done and this returns false
     */
    public function addDncForContact(
        $contact,
        $channel,
        $reason = DNC::BOUNCED,
        $comments = '',
        $persist = true,
        $checkCurrentStatus = true,
        $allowUnsubscribeOverride = false,
    ) {
        $dnc     = false;
        if (is_numeric($contact)) {
            $contact = $this->leadModel->getEntity($contact);
        }

        if (null === $contact) {
            // Contact not found, nothing to do
            return false;
        }

        // if !$checkCurrentStatus, assume is contactable due to already being validated
        $isContactable = ($checkCurrentStatus) ? $this->isContactable($contact, $channel) : DNC::IS_CONTACTABLE;

        /** @var ArrayCollection<int, DNC> $dncEntities */
        $dncEntities = new ArrayCollection();
        // If they don't have a DNC entry yet
        if (DNC::IS_CONTACTABLE === $isContactable) {
            $dnc = $dncEntities[] = $this->createDncRecord($contact, $channel, $reason, $comments);
        } elseif ($isContactable !== $reason) {
            // Or if the given reason is different than the stated reason

            $dncEntities = $contact->getDoNotContact();
            foreach ($dncEntities as $dnc) {
                // Only update if the contact did not unsubscribe themselves or if the code forces it
                $allowOverride = ($allowUnsubscribeOverride || DNC::UNSUBSCRIBED !== $dnc->getReason());

                // Only update if the contact did not unsubscribe themselves
                if ($allowOverride && $dnc->getChannel() === $channel) {
                    // Note the outdated entry for listeners
                    $contact->removeDoNotContactEntry($dnc);

                    // Update the entry with the latest
                    $this->updateDncRecord($dnc, $contact, $channel, $reason, $comments);

                    break;
                }
            }
        }

        if (null !== $dnc && $persist) {
            // Use model saveEntity to trigger events for DNC change
            $this->leadModel->saveEntity($contact);
            $this->dncRepo->detachEntities($dncEntities->toArray());
            // need to force a collection to load items in the next call.
            $collection = $contact->getDoNotContact();
            if ($collection instanceof PersistentCollection) {
                $collection->setInitialized(false);
            }
        }

        return $dnc;
    }

    /**
     * @param string $channel
     *
     * @return int
     *
     * @see DNC This method can return boolean false, so be
     *                                             sure to always compare the return value against
     *                                             the class constants of DoNotContact
     */
    public function isContactable(Lead $contact, $channel)
    {
        if (is_array($channel)) {
            $channel = key($channel);
        }

        $dncEntries = $this->dncRepo->getEntriesByLeadAndChannel($contact, $channel);

        // If the lead has no entries in the DNC table, we're good to go
        if (empty($dncEntries)) {
            return DNC::IS_CONTACTABLE;
        }

        foreach ($dncEntries as $dnc) {
            if (DNC::IS_CONTACTABLE !== $dnc->getReason()) {
                return $dnc->getReason();
            }
        }

        return DNC::IS_CONTACTABLE;
    }

    public function createDncRecord(Lead $contact, $channel, $reason, $comments = null): DNC
    {
        $dnc = new DNC();

        if (is_array($channel)) {
            $channelId = reset($channel);
            $channel   = key($channel);

            $dnc->setChannelId((int) $channelId);
        }

        $dnc->setChannel($channel);
        $dnc->setReason($reason);
        $dnc->setLead($contact);
        $dnc->setDateAdded(new \DateTime());
        $dnc->setComments($comments);

        $contact->addDoNotContactEntry($dnc);

        return $dnc;
    }

    public function updateDncRecord(DNC $dnc, Lead $contact, $channel, $reason, $comments = null): void
    {
        // Update the DNC entry
        $dnc->setChannel($channel);
        $dnc->setReason($reason);
        $dnc->setLead($contact);
        $dnc->setDateAdded(new \DateTime());
        $dnc->setComments($comments);

        // Re-add the entry to the lead
        $contact->addDoNotContactEntry($dnc);
    }

    /**
     * Get all available reason-channel combinations.
     *
     * @return array<array{reason: int, channel: string}>
     */
    public function getReasonChannelCombinations(bool $useCache = true): array
    {
        $cacheTimeout = (int) $this->coreParametersHelper->get('cached_data_timeout');
        $cacheItem    = $this->cacheProvider->getItem('dnc.reason_channel_combinations');
        if ($useCache && $cacheItem->isHit()) {
            return $cacheItem->get();
        } else {
            $reasonChannelCombinations = $this->dncRepo->getReasonChannelCombinations();
            $cacheItem->set($reasonChannelCombinations);
            $cacheItem->expiresAfter($cacheTimeout * 60);
            $this->cacheProvider->save($cacheItem);

            return $reasonChannelCombinations;
        }
    }

    /**
     * @return DoNotContactRepository
     */
    public function getDncRepo()
    {
        return $this->dncRepo;
    }
}
