<?php

namespace Mautic\LeadBundle\Model;

use Doctrine\ORM\EntityManagerInterface;
use Mautic\CategoryBundle\Model\CategoryModel;
use Mautic\CoreBundle\Helper\Chart\BarChart;
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
use Mautic\CoreBundle\Helper\Chart\LineChart;
use Mautic\CoreBundle\Helper\Chart\PieChart;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\CoreBundle\Helper\ProgressBarHelper;
use Mautic\CoreBundle\Helper\UserHelper;
use Mautic\CoreBundle\Model\FormModel;
use Mautic\CoreBundle\Model\GlobalSearchInterface;
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
use Mautic\CoreBundle\Translation\Translator;
use Mautic\LeadBundle\Entity\DoNotContactRepository;
use Mautic\LeadBundle\Entity\Lead;
use Mautic\LeadBundle\Entity\LeadField;
use Mautic\LeadBundle\Entity\LeadList;
use Mautic\LeadBundle\Entity\LeadListRepository;
use Mautic\LeadBundle\Entity\ListLead;
use Mautic\LeadBundle\Entity\ListLeadRepository;
use Mautic\LeadBundle\Entity\OperatorListTrait;
use Mautic\LeadBundle\Event\LeadListEvent;
use Mautic\LeadBundle\Event\LeadListFiltersChoicesEvent;
use Mautic\LeadBundle\Event\ListChangeEvent;
use Mautic\LeadBundle\Event\ListPreProcessListEvent;
use Mautic\LeadBundle\Form\Type\ListType;
use Mautic\LeadBundle\Helper\SegmentCountCacheHelper;
use Mautic\LeadBundle\LeadEvents;
use Mautic\LeadBundle\Segment\ContactSegmentService;
use Mautic\LeadBundle\Segment\Exception\FieldNotFoundException;
use Mautic\LeadBundle\Segment\Exception\SegmentNotFoundException;
use Mautic\LeadBundle\Segment\Exception\TableNotFoundException;
use Mautic\LeadBundle\Segment\Stat\ChartQuery\SegmentContactsLineChartQuery;
use Mautic\LeadBundle\Segment\Stat\SegmentChartQueryFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * @extends FormModel<LeadList>
 */
class ListModel extends FormModel implements GlobalSearchInterface
{
    use OperatorListTrait;

    /**
     * @var mixed[]
     */
    private array $choiceFieldsCache = [];

    public function __construct(
        protected CategoryModel $categoryModel,
        CoreParametersHelper $coreParametersHelper,
        private ContactSegmentService $leadSegmentService,
        private SegmentChartQueryFactory $segmentChartQueryFactory,
        private RequestStack $requestStack,
        private SegmentCountCacheHelper $segmentCountCacheHelper,
        private DoNotContactRepository $doNotContactRepository,
        EntityManagerInterface $em,
        CorePermissions $security,
        EventDispatcherInterface $dispatcher,
        UrlGeneratorInterface $router,
        Translator $translator,
        UserHelper $userHelper,
        LoggerInterface $mauticLogger,
    ) {
        parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
    }

    /**
     * Used by addLead and removeLead functions.
     */
    private array $leadChangeLists = [];

    /**
     * @return LeadListRepository
     */
    public function getRepository()
    {
        /** @var LeadListRepository $repo */
        $repo = $this->em->getRepository(LeadList::class);

        $repo->setDispatcher($this->dispatcher);
        $repo->setTranslator($this->translator);

        return $repo;
    }

    /**
     * Returns the repository for the table that houses the leads associated with a list.
     *
     * @return ListLeadRepository
     */
    public function getListLeadRepository()
    {
        return $this->em->getRepository(ListLead::class);
    }

    public function getPermissionBase(): string
    {
        return 'lead:lists';
    }

    /**
     * @param bool $unlock
     *
     * @throws \Doctrine\DBAL\Exception
     */
    public function saveEntity($entity, $unlock = true): void
    {
        $isNew = ($entity->getId()) ? false : true;

        // set some defaults
        $this->setTimestamps($entity, $isNew, $unlock);

        $alias = $entity->getAlias();
        if (empty($alias)) {
            $alias = $entity->getName();
        }
        $alias = $this->cleanAlias($alias, '', 0, '-');

        // make sure alias is not already taken
        $repo      = $this->getRepository();
        $testAlias = $alias;
        $existing  = $repo->getLists(null, $testAlias, $entity->getId(), false);
        $count     = count($existing);
        $aliasTag  = $count;

        while ($count) {
            $testAlias = $alias.$aliasTag;
            $existing  = $repo->getLists(null, $testAlias, $entity->getId(), false);
            $count     = count($existing);
            ++$aliasTag;
        }
        if ($testAlias != $alias) {
            $alias = $testAlias;
        }
        $entity->setAlias($alias);

        $publicName = $entity->getPublicName();
        if (empty($publicName)) {
            $entity->setPublicName($entity->getName());
        }

        $event = $this->dispatchEvent('pre_save', $entity, $isNew);
        $repo->saveEntity($entity);
        $this->dispatchEvent('post_save', $entity, $isNew, $event);
    }

    /**
     * @param array<int> $ids
     *
     * @return array<object>
     */
    public function deleteEntities($ids): array
    {
        $entities = [];
        foreach ($ids as $listId) {
            $leadList = $this->getEntity($listId);
            if ($leadList) {
                $entities[$listId] = $leadList;
                $this->deleteEntity($leadList);
            }
        }

        return $entities;
    }

    /**
     * @param LeadList $entity
     */
    public function deleteEntity($entity): void
    {
        $id    = $entity->getId();
        $this->dispatchEvent('pre_delete', $entity);
        $this->getRepository()->setSegmentAsDeleted($id);

        $entity->deletedId = $id;
        $this->dispatcher->dispatch(new LeadListEvent($entity), LeadEvents::ON_LIST_DELETE);
        $entity->setId(null);
    }

    public function hardDeleteEntity(LeadList $leadList): void
    {
        $leadList->deletedId = $leadList->getId();
        $this->getRepository()->deleteEntity($leadList);
        $this->dispatchEvent('post_delete', $leadList);
    }

    /**
     * @param string|null $action
     * @param array       $options
     *
     * @return FormInterface<LeadList>
     */
    public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): FormInterface
    {
        if (!$entity instanceof LeadList) {
            throw new MethodNotAllowedHttpException(['LeadList'], 'Entity must be of class LeadList()');
        }

        if (!empty($action)) {
            $options['action'] = $action;
        }

        return $formFactory->create(ListType::class, $entity, $options);
    }

    /**
     * Get a specific entity or generate a new one if id is empty.
     */
    public function getEntity($id = null): ?LeadList
    {
        if (null === $id) {
            return new LeadList();
        }

        return parent::getEntity($id);
    }

    public function getSoftDeletedEntity(int $id): ?LeadList
    {
        return $this->getRepository()->getSoftDeletedEntity($id);
    }

    /**
     * @throws MethodNotAllowedHttpException
     */
    protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
    {
        if (!$entity instanceof LeadList) {
            throw new MethodNotAllowedHttpException(['LeadList'], 'Entity must be of class LeadList()');
        }

        switch ($action) {
            case 'pre_save':
                $name = LeadEvents::LIST_PRE_SAVE;
                break;
            case 'post_save':
                $name = LeadEvents::LIST_POST_SAVE;
                break;
            case 'pre_delete':
                $name = LeadEvents::LIST_PRE_DELETE;
                break;
            case 'post_delete':
                $name = LeadEvents::LIST_POST_DELETE;
                break;
            case 'pre_unpublish':
                $name = LeadEvents::LIST_PRE_UNPUBLISH;
                break;
            default:
                return null;
        }

        if ($this->dispatcher->hasListeners($name)) {
            if (empty($event)) {
                $event = new LeadListEvent($entity, $isNew);
                $event->setEntityManager($this->em);
            }
            $this->dispatcher->dispatch($event, $name);

            return $event;
        }

        return null;
    }

    /**
     * Get a list of field choices for filters.
     *
     * @return mixed[]
     */
    public function getChoiceFields(string $search = ''): array
    {
        if ($this->choiceFieldsCache) {
            return $this->choiceFieldsCache;
        }

        $choices = [];

        $choices['lead']['tags'] =
            [
                'label'      => $this->translator->trans('mautic.lead.list.filter.tags'),
                'properties' => [
                    'type' => 'tags',
                ],
                'operators'  => $this->getOperatorsForFieldType('multiselect'),
                'object'     => 'lead',
            ];

        // Add custom choices
        if ($this->dispatcher->hasListeners(LeadEvents::LIST_FILTERS_CHOICES_ON_GENERATE)) {
            $event = new LeadListFiltersChoicesEvent([], $this->getOperatorsForFieldType(), $this->translator, $this->requestStack->getCurrentRequest(), $search);
            $this->dispatcher->dispatch($event, LeadEvents::LIST_FILTERS_CHOICES_ON_GENERATE);
            $choices = $event->getChoices();
        }

        // Order choices by label.
        foreach ($choices as $key => $choice) {
            $cmp = fn ($a, $b): int => strcmp($a['label'], $b['label']);
            uasort($choice, $cmp);
            $choices[$key] = $choice;
        }

        $this->choiceFieldsCache = $choices;

        return $choices;
    }

    /**
     * @param string $alias
     *
     * @return array
     */
    public function getUserLists($alias = '')
    {
        $user = !$this->security->isGranted('lead:lists:viewother') ? $this->userHelper->getUser() : null;

        return $this->em->getRepository(LeadList::class)->getLists($user, $alias);
    }

    /**
     * Get a list of global lead lists.
     *
     * @return mixed
     */
    public function getGlobalLists()
    {
        return $this->em->getRepository(LeadList::class)->getGlobalLists();
    }

    /**
     * Get a list of preference center lead lists.
     *
     * @return mixed
     */
    public function getPreferenceCenterLists()
    {
        return $this->em->getRepository(LeadList::class)->getPreferenceCenterList();
    }

    /**
     * @param int      $limit
     * @param bool|int $maxLeads
     *
     * @throws \Exception
     */
    public function rebuildListLeads(LeadList $leadList, $limit = 100, $maxLeads = false, ?OutputInterface $output = null): int
    {
        defined('MAUTIC_REBUILDING_LEAD_LISTS') or define('MAUTIC_REBUILDING_LEAD_LISTS', 1);

        $segmentId = $leadList->getId();

        $dtHelper = new DateTimeHelper();

        $batchLimiters = ['dateTime' => $dtHelper->toUtcString()];
        $list          = ['id' => $segmentId, 'filters' => $leadList->getFilters()];

        $this->dispatcher->dispatch(
            new ListPreProcessListEvent($list, false), LeadEvents::LIST_PRE_PROCESS_LIST
        );

        try {
            // Get a count of leads to add
            $newLeadsCount = $this->leadSegmentService->getNewLeadListLeadsCount($leadList, $batchLimiters);
        } catch (FieldNotFoundException) {
            // A field from filter does not exist anymore. Do not rebuild.
            return 0;
        } catch (SegmentNotFoundException) {
            // A segment from filter does not exist anymore. Do not rebuild.
            return 0;
        } catch (TableNotFoundException $e) {
            // Invalid filter table, filter definition is not well asset or it is deleted.  Do not rebuild but log.
            $this->logger->error($e->getMessage());

            return 0;
        }

        // Ensure the same list is used each batch <- would love to know how
        $batchLimiters['maxId'] = (int) $newLeadsCount[$segmentId]['maxId'];

        // Number of total leads to process
        $leadCount = (int) $newLeadsCount[$segmentId]['count'];

        $this->logger->info('Segment QB - No new leads for segment found');

        if ($output) {
            $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_added', ['%leads%' => $leadCount, '%batch%' => $limit]));
        }

        // Handle by batches
        $start = $leadsProcessed = 0;

        // Try to save some memory
        gc_enable();

        if ($leadCount) {
            $maxCount = $maxLeads ?: $leadCount;

            if ($output) {
                $progress = ProgressBarHelper::init($output, $maxCount);
                $progress->start();
            }

            // Add leads
            while ($start < $leadCount) {
                // Keep CPU down for large lists; sleep per $limit batch
                $this->batchSleep();

                $this->logger->debug(sprintf('Segment QB - Fetching new leads for segment [%d] %s', $segmentId, $leadList->getName()));
                $newLeadList = $this->leadSegmentService->getNewLeadListLeads($leadList, $batchLimiters, $limit);

                if (empty($newLeadList[$segmentId])) {
                    // Somehow ran out of leads so break out
                    break;
                }

                $this->logger->debug(sprintf('Segment QB - Adding %d new leads to segment [%d] %s', count($newLeadList[$segmentId]), $segmentId, $leadList->getName()));
                foreach ($newLeadList[$segmentId] as $l) {
                    $this->logger->debug(sprintf('Segment QB - Adding lead #%s to segment [%d] %s', $l['id'], $segmentId, $leadList->getName()));

                    $this->addLead($l, $leadList, false, true, -1, $dtHelper->getLocalDateTime());

                    ++$leadsProcessed;
                    if ($output && $leadsProcessed < $maxCount) {
                        $progress->setProgress($leadsProcessed);
                    }

                    if ($maxLeads && $leadsProcessed >= $maxLeads) {
                        break;
                    }
                }

                $this->logger->info(sprintf('Segment QB - Added %d new leads to segment [%d] %s', count($newLeadList[$segmentId]), $segmentId, $leadList->getName()));

                $start += $limit;

                // Dispatch batch event
                if ($this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) {
                    $this->dispatcher->dispatch(
                        new ListChangeEvent($newLeadList[$segmentId], $leadList, true),
                        LeadEvents::LEAD_LIST_BATCH_CHANGE
                    );
                }

                unset($newLeadList);

                // Free some memory
                gc_collect_cycles();

                if ($maxLeads && $leadsProcessed >= $maxLeads) {
                    if ($output) {
                        $progress->finish();
                        $output->writeln('');
                    }

                    return $leadsProcessed;
                }
            }

            if ($output) {
                $progress->finish();
                $output->writeln('');
            }
        }

        // Unset max ID to prevent capping at newly added max ID
        unset($batchLimiters['maxId']);

        $orphanLeadsCount = $this->leadSegmentService->getOrphanedLeadListLeadsCount($leadList);

        // Ensure the same list is used each batch
        $batchLimiters['maxId'] = (int) $orphanLeadsCount[$segmentId]['maxId'];

        // Restart batching
        $start     = 0;
        $leadCount = $orphanLeadsCount[$segmentId]['count'];

        if ($output) {
            $output->writeln($this->translator->trans('mautic.lead.list.rebuild.to_be_removed', ['%leads%' => $leadCount, '%batch%' => $limit]));
        }

        if ($leadCount) {
            $maxCount = $maxLeads ?: $leadCount;

            if ($output) {
                $progress = ProgressBarHelper::init($output, $maxCount);
                $progress->start();
            }

            // Remove leads
            while ($start < $leadCount) {
                // Keep CPU down for large lists; sleep per $limit batch
                $this->batchSleep();

                $removeLeadList = $this->leadSegmentService->getOrphanedLeadListLeads($leadList, [], $limit);

                if (empty($removeLeadList[$segmentId])) {
                    // Somehow ran out of leads so break out
                    break;
                }

                $processedLeads = [];
                foreach ($removeLeadList[$segmentId] as $l) {
                    $this->removeLead($l, $leadList, false, true, true);
                    $processedLeads[] = $l;
                    ++$leadsProcessed;
                    if ($output && $leadsProcessed < $maxCount) {
                        $progress->setProgress($leadsProcessed);
                    }

                    if ($maxLeads && $leadsProcessed >= $maxLeads) {
                        break;
                    }
                }

                // Dispatch batch event
                if (count($processedLeads) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_BATCH_CHANGE)) {
                    $this->dispatcher->dispatch(
                        new ListChangeEvent($processedLeads, $leadList, false),
                        LeadEvents::LEAD_LIST_BATCH_CHANGE
                    );
                }

                $start += $limit;

                unset($removeLeadList);

                // Free some memory
                gc_collect_cycles();

                if ($maxLeads && $leadsProcessed >= $maxLeads) {
                    if ($output) {
                        $progress->finish();
                        $output->writeln('');
                    }

                    return $leadsProcessed;
                }
            }

            if ($output) {
                $progress->finish();
                $output->writeln('');
            }
        }

        if ($this->coreParametersHelper->get('update_segment_contact_count_in_background', false)) {
            $this->segmentCountCacheHelper->invalidateSegmentContactCount($segmentId);
        } else {
            $totalLeadCount = $this->getRepository()->getLeadCount($segmentId);
            $this->segmentCountCacheHelper->setSegmentContactCount($segmentId, (int) $totalLeadCount);
        }

        return $leadsProcessed;
    }

    /**
     * Add lead to lists.
     *
     * @param array|int|Lead $lead
     * @param array|LeadList $lists
     * @param bool           $manuallyAdded
     * @param bool           $batchProcess
     * @param int            $searchListLead  0 = reference, 1 = yes, -1 = known to not exist
     * @param \DateTime      $dateManipulated
     *
     * @throws \Exception
     */
    public function addLead($lead, $lists, $manuallyAdded = false, $batchProcess = false, $searchListLead = 1, $dateManipulated = null): void
    {
        if (null == $dateManipulated) {
            $dateManipulated = new \DateTime();
        }

        if (!$lead instanceof Lead) {
            $leadId = (is_array($lead) && isset($lead['id'])) ? $lead['id'] : $lead;
            $lead   = $this->em->getReference(Lead::class, $leadId);
        } else {
            $leadId = $lead->getId();
        }

        if (!$lists instanceof LeadList) {
            // make sure they are ints
            $searchForLists = [];
            foreach ($lists as &$l) {
                $l = (int) $l;
                if (!isset($this->leadChangeLists[$l])) {
                    $searchForLists[] = $l;
                }
            }

            if (!empty($searchForLists)) {
                $listEntities = $this->getEntities([
                    'filter' => [
                        'force' => [
                            [
                                'column' => 'l.id',
                                'expr'   => 'in',
                                'value'  => $searchForLists,
                            ],
                        ],
                    ],
                ]);

                foreach ($listEntities as $list) {
                    $this->leadChangeLists[$list->getId()] = $list;
                }
            }

            unset($listEntities, $searchForLists);
        } else {
            $this->leadChangeLists[$lists->getId()] = $lists;

            $lists = [$lists->getId()];
        }

        if (!is_array($lists)) {
            $lists = [$lists];
        }

        $persistLists   = [];
        $dispatchEvents = [];

        foreach ($lists as $listId) {
            if (!isset($this->leadChangeLists[$listId])) {
                // List no longer exists in the DB so continue to the next
                continue;
            }

            if (-1 == $searchListLead) {
                $listLead = null;
            } elseif ($searchListLead) {
                $listLead = $this->getListLeadRepository()->findOneBy(
                    [
                        'lead' => $lead,
                        'list' => $this->leadChangeLists[$listId],
                    ]
                );
            } else {
                $listLead = $this->em->getReference(ListLead::class,
                    [
                        'lead' => $leadId,
                        'list' => $listId,
                    ]
                );
            }

            if (null != $listLead) {
                if ($manuallyAdded && $listLead->wasManuallyRemoved()) {
                    $listLead->setManuallyRemoved(false);
                    $listLead->setManuallyAdded($manuallyAdded);

                    $persistLists[]   = $listLead;
                    $dispatchEvents[] = $listId;
                } else {
                    // Detach from Doctrine
                    $this->em->detach($listLead);

                    continue;
                }
            } else {
                $listLead = new ListLead();
                $listLead->setList($this->em->getReference(LeadList::class, $listId));
                $listLead->setLead($lead);
                $listLead->setManuallyAdded($manuallyAdded);
                $listLead->setDateAdded($dateManipulated);

                $persistLists[]   = $listLead;
                $dispatchEvents[] = $listId;
            }

            if ($this->coreParametersHelper->get('update_segment_contact_count_in_background', false)) {
                $this->segmentCountCacheHelper->invalidateSegmentContactCount($listId);
            } else {
                $this->segmentCountCacheHelper->incrementSegmentContactCount($listId);
            }
        }

        if (!empty($persistLists)) {
            $this->getRepository()->saveEntities($persistLists);
        }

        // Clear ListLead entities from Doctrine memory
        $this->getRepository()->detachEntities($persistLists);

        if ($batchProcess) {
            // Detach for batch processing to preserve memory
            $this->em->detach($lead);
        } elseif (!empty($dispatchEvents) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_CHANGE)) {
            foreach ($dispatchEvents as $listId) {
                $event = new ListChangeEvent($lead, $this->leadChangeLists[$listId]);
                $this->dispatcher->dispatch($event, LeadEvents::LEAD_LIST_CHANGE);

                unset($event);
            }
        }

        unset($lead, $persistLists, $lists);
    }

    /**
     * Remove a lead from lists.
     *
     * @param bool $manuallyRemoved
     * @param bool $batchProcess
     * @param bool $skipFindOne
     *
     * @throws \Exception
     */
    public function removeLead($lead, $lists, $manuallyRemoved = false, $batchProcess = false, $skipFindOne = false): void
    {
        if (!$lead instanceof Lead) {
            $leadId = (is_array($lead) && isset($lead['id'])) ? $lead['id'] : $lead;
            $lead   = $this->em->getReference(Lead::class, $leadId);
        } else {
            $leadId = $lead->getId();
        }

        if (!$lists instanceof LeadList) {
            // make sure they are ints
            $searchForLists = [];
            foreach ($lists as &$l) {
                $l = (int) $l;
                if (!isset($this->leadChangeLists[$l])) {
                    $searchForLists[] = $l;
                }
            }

            if (!empty($searchForLists)) {
                $listEntities = $this->getEntities([
                    'filter' => [
                        'force' => [
                            [
                                'column' => 'l.id',
                                'expr'   => 'in',
                                'value'  => $searchForLists,
                            ],
                        ],
                    ],
                ]);

                foreach ($listEntities as $list) {
                    $this->leadChangeLists[$list->getId()] = $list;
                }
            }

            unset($listEntities, $searchForLists);
        } else {
            $this->leadChangeLists[$lists->getId()] = $lists;

            $lists = [$lists->getId()];
        }

        if (!is_array($lists)) {
            $lists = [$lists];
        }

        $persistLists   = [];
        $deleteLists    = [];
        $dispatchEvents = [];

        foreach ($lists as $listId) {
            if (!isset($this->leadChangeLists[$listId])) {
                // List no longer exists in the DB so continue to the next
                continue;
            }

            $listLead = (!$skipFindOne) ?
                $this->getListLeadRepository()->findOneBy([
                    'lead' => $lead,
                    'list' => $this->leadChangeLists[$listId],
                ]) :
                $this->em->getReference(ListLead::class, [
                    'lead' => $leadId,
                    'list' => $listId,
                ]);

            if (null == $listLead) {
                // Lead is not part of this list
                continue;
            }

            if (($manuallyRemoved && $listLead->wasManuallyAdded()) || (!$manuallyRemoved && !$listLead->wasManuallyAdded())) {
                // lead was manually added and now manually removed or was not manually added and now being removed
                $deleteLists[]    = $listLead;
                $dispatchEvents[] = $listId;
            } elseif ($manuallyRemoved && !$listLead->wasManuallyAdded()) {
                $listLead->setManuallyRemoved(true);

                $persistLists[]   = $listLead;
                $dispatchEvents[] = $listId;
            }

            if ($this->coreParametersHelper->get('update_segment_contact_count_in_background', false)) {
                $this->segmentCountCacheHelper->invalidateSegmentContactCount($listId);
            } else {
                $this->segmentCountCacheHelper->decrementSegmentContactCount($listId);
            }

            unset($listLead);
        }

        if (!empty($persistLists)) {
            $this->getRepository()->saveEntities($persistLists);
        }

        if (!empty($deleteLists)) {
            $this->getRepository()->deleteEntities($deleteLists);
        }

        // Clear ListLead entities from Doctrine memory
        $this->getListLeadRepository()->detachEntities($persistLists);
        $this->getListLeadRepository()->detachEntities($deleteLists);

        if ($batchProcess) {
            // Detach for batch processing to preserve memory
            $this->em->detach($lead);
        } elseif (!empty($dispatchEvents) && $this->dispatcher->hasListeners(LeadEvents::LEAD_LIST_CHANGE)) {
            foreach ($dispatchEvents as $listId) {
                $event = new ListChangeEvent($lead, $this->leadChangeLists[$listId], false);
                $this->dispatcher->dispatch($event, LeadEvents::LEAD_LIST_CHANGE);

                unset($event);
            }
        }

        unset($lead, $deleteLists, $persistLists, $lists);
    }

    public function removeLeadsByListId(int $listId): void
    {
        $this->getListLeadRepository()->removeLeadsByListId($listId);
    }

    /**
     * Batch sleep according to settings.
     */
    protected function batchSleep()
    {
        $leadSleepTime = $this->coreParametersHelper->get('batch_lead_sleep_time', false);
        if (false === $leadSleepTime) {
            $leadSleepTime = $this->coreParametersHelper->get('batch_sleep_time', 1);
        }

        if (empty($leadSleepTime)) {
            return;
        }

        if ($leadSleepTime < 1) {
            usleep($leadSleepTime * 1_000_000);
        } else {
            sleep($leadSleepTime);
        }
    }

    /**
     * Get a list of top (by leads added) lists.
     *
     * @param int       $limit
     * @param \DateTime $dateFrom
     * @param \DateTime $dateTo
     * @param bool      $canViewOthers
     */
    public function getTopLists($limit = 10, $dateFrom = null, $dateTo = null, $canViewOthers = true): array
    {
        $q = $this->em->getConnection()->createQueryBuilder();
        $q->select('COUNT(t.date_added) AS leads, ll.id, ll.name, ll.alias')
            ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 't')
            ->join('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'll.id = t.leadlist_id')
            ->orderBy('leads', 'DESC')
            ->where($q->expr()->eq('ll.is_published', ':published'))
            ->setParameter('published', true)
            ->groupBy('ll.id')
            ->setMaxResults($limit);

        if (!$canViewOthers) {
            $q->andWhere('ll.created_by = :userId')
                ->setParameter('userId', $this->userHelper->getUser()->getId());
        }

        $chartQuery = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);
        $chartQuery->applyDateFilters($q, 'date_added');

        return $q->executeQuery()->fetchAllAssociative();
    }

    /**
     * Get a list of top (by leads added) lists.
     *
     * @param int    $limit
     * @param string $dateFrom
     * @param string $dateTo
     *
     * @return array
     */
    public function getLifeCycleSegments($limit, $dateFrom, $dateTo, $canViewOthers, $segments)
    {
        if (!empty($segments)) {
            $segmentlist = "'".implode("','", $segments)."'";
        }
        $q = $this->em->getConnection()->createQueryBuilder();
        $q->select('COUNT(t.date_added) AS leads, ll.id, ll.name as name,ll.alias as alias')
            ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 't')
            ->join('t', MAUTIC_TABLE_PREFIX.'lead_lists', 'll', 'll.id = t.leadlist_id')
            ->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id')
            ->orderBy('leads', 'DESC')
            ->where($q->expr()->eq('ll.is_published', ':published'))
            ->setParameter('published', true)
            ->groupBy('ll.id');

        if ($limit) {
            $q->setMaxResults($limit);
        }
        if (!empty($segments)) {
            $q->andWhere('ll.id IN ('.$segmentlist.')');
        }
        if (!empty($dateFrom)) {
            $q->andWhere("l.date_added >= '".$dateFrom->format('Y-m-d')."'");
        }
        if (!empty($dateTo)) {
            $q->andWhere("l.date_added <= '".$dateTo->format('Y-m-d')." 23:59:59'");
        }
        if (!$canViewOthers) {
            $q->andWhere('ll.created_by = :userId')
                ->setParameter('userId', $this->userHelper->getUser()->getId());
        }

        $results = $q->executeQuery()->fetchAllAssociative();

        if (in_array(0, $segments)) {
            $qAll = $this->em->getConnection()->createQueryBuilder();
            $qAll->select('COUNT(t.date_added) AS leads, 0 as id, "All Contacts" as name, "" as alias')
                ->from(MAUTIC_TABLE_PREFIX.'leads', 't');

            if (!$canViewOthers) {
                $qAll->andWhere('ll.created_by = :userId')
                    ->setParameter('userId', $this->userHelper->getUser()->getId());
            }
            if (!empty($dateFrom)) {
                $qAll->andWhere("t.date_added >= '".$dateFrom->format('Y-m-d')."'");
            }
            if (!empty($dateTo)) {
                $qAll->andWhere("t.date_added <= '".$dateTo->format('Y-m-d')." 23:59:59'");
            }
            $resultsAll = $qAll->executeQuery()->fetchAllAssociative();
            $results    = array_merge($results, $resultsAll);
        }

        return $results;
    }

    /**
     * @param bool $canViewOthers
     */
    public function getLifeCycleSegmentChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat, $filter, $canViewOthers, $listName): array
    {
        $chart = new PieChart();
        $query = new ChartQuery($this->em->getConnection(), $dateFrom, $dateTo);

        if (!$canViewOthers) {
            $filter['owner_id'] = $this->userHelper->getUser()->getId();
        }

        if (isset($filter['flag'])) {
            unset($filter['flag']);
        }

        $allLists   = $query->getCountQuery('leads', 'id', 'date_added', null);
        $lists      = $query->count('leads', 'id', 'date_added', $filter, null);
        $all        = $query->fetchCount($allLists);
        $identified = $lists;

        $chart->setDataset($listName, $identified);

        if (isset($filter['leadlist_id']['value'])) {
            $chart->setDataset(
                $this->translator->trans('mautic.lead.lifecycle.graph.pie.all.lists'),
                $all
            );
        }

        return $chart->render(false);
    }

    /**
     * @param array $filter
     * @param bool  $canViewOthers
     */
    public function getStagesBarChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
    {
        $data['values'] = [];
        $data['labels'] = [];

        $q = $this->em->getConnection()->createQueryBuilder();

        $q->select('count(l.id) as leads, s.name as stage')
            ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 't')
            ->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id')
            ->join('t', MAUTIC_TABLE_PREFIX.'stages', 's', 's.id=l.stage_id')
            ->orderBy('leads', 'DESC')
            ->where($q->expr()->eq('s.is_published', ':published'))

            ->andWhere($q->expr()->gte('t.date_added', ':date_from'))
            ->setParameter('date_from', $dateFrom->format('Y-m-d'))
            ->andWhere($q->expr()->lte('t.date_added', ':date_to'))
            ->setParameter('date_to', $dateTo->format('Y-m-d 23:59:59'))
            ->setParameter('published', true);

        if (isset($filter['leadlist_id']['value'])) {
            $q->andWhere($q->expr()->eq('t.leadlist_id', ':leadlistid'))->setParameter('leadlistid', $filter['leadlist_id']['value']);
        }

        $q->groupBy('s.name');

        if (!$canViewOthers) {
            $q->andWhere('s.created_by = :userId')
                ->setParameter('userId', $this->userHelper->getUser()->getId());
        }

        $results = $q->executeQuery()->fetchAllAssociative();

        foreach ($results as $result) {
            $data['labels'][] = substr($result['stage'], 0, 12);
            $data['values'][] = $result['leads'];
        }
        $data['xAxes'][] = ['display' => true];
        $data['yAxes'][] = ['display' => true];

        $baseData = [
            'label' => $this->translator->trans('mautic.lead.leads'),
            'data'  => $data['values'],
        ];

        $chart      = new BarChart($data['labels']);
        $datasets[] = array_merge($baseData, $chart->generateColors(3));

        return [
            'labels'   => $data['labels'],
            'datasets' => $datasets,
            'options'  => [
                'xAxes' => $data['xAxes'],
                'yAxes' => $data['yAxes'],
            ], ];
    }

    /**
     * @param array $filter
     * @param bool  $canViewOthers
     */
    public function getDeviceGranularityData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = [], $canViewOthers = true): array
    {
        $data['values'] = [];
        $data['labels'] = [];

        $q = $this->em->getConnection()->createQueryBuilder();

        $q->select('count(l.id) as leads, ds.device')
            ->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 't')
            ->join('t', MAUTIC_TABLE_PREFIX.'leads', 'l', 'l.id = t.lead_id')
            ->join('t', MAUTIC_TABLE_PREFIX.'page_hits', 'h', 'h.lead_id=l.id')
            ->join('h', MAUTIC_TABLE_PREFIX.'lead_devices', 'ds', 'ds.id = h.device_id')
            ->orderBy('ds.device', 'DESC')
            ->andWhere($q->expr()->gte('t.date_added', ':date_from'))
            ->setParameter('date_from', $dateFrom->format('Y-m-d'))
            ->andWhere($q->expr()->lte('t.date_added', ':date_to'))
            ->setParameter('date_to', $dateTo->format('Y-m-d 23:59:59'));

        if (isset($filter['leadlist_id']['value'])) {
            $q->andWhere($q->expr()->eq('t.leadlist_id', ':leadlistid'))->setParameter(
                'leadlistid',
                $filter['leadlist_id']['value']
            );
        }

        $q->groupBy('ds.device');

        if (!$canViewOthers) {
            $q->andWhere('l.created_by = :userId')
                ->setParameter('userId', $this->userHelper->getUser()->getId());
        }

        $results = $q->executeQuery()->fetchAllAssociative();

        foreach ($results as $result) {
            $data['labels'][] = substr(empty($result['device']) ? $this->translator->trans('mautic.core.no.info') : $result['device'], 0, 12);
            $data['values'][] = $result['leads'];
        }

        $data['xAxes'][] = ['display' => true];
        $data['yAxes'][] = ['display' => true];

        $baseData = [
            'label' => $this->translator->trans('mautic.core.device'),
            'data'  => $data['values'],
        ];

        $chart      = new BarChart($data['labels']);
        $datasets[] = array_merge($baseData, $chart->generateColors(2));

        return [
            'labels'   => $data['labels'],
            'datasets' => $datasets,
            'options'  => [
                'xAxes' => $data['xAxes'],
                'yAxes' => $data['yAxes'],
            ],
        ];
    }

    /**
     * Get line chart data of hits.
     *
     * @param string $unit       {@link php.net/manual/en/function.date.php#refsect1-function.date-parameters}
     * @param string $dateFormat
     * @param array  $filter
     */
    public function getSegmentContactsLineChartData($unit, \DateTime $dateFrom, \DateTime $dateTo, $dateFormat = null, $filter = []): array
    {
        $chart    = new LineChart($unit, $dateFrom, $dateTo, $dateFormat);
        $query    = new SegmentContactsLineChartQuery($this->em->getConnection(), $dateFrom, $dateTo, $filter);

        // added line everytime
        $chart->setDataset($this->translator->trans('mautic.lead.segments.contacts.added'), $this->segmentChartQueryFactory->getContactsAdded($query));
        $chart->setDataset($this->translator->trans('mautic.lead.segments.contacts.removed'), $this->segmentChartQueryFactory->getContactsRemoved($query));
        $chart->setDataset($this->translator->trans('mautic.lead.segments.contacts.total'), $this->segmentChartQueryFactory->getContactsTotal($query, $this));

        return $chart->render();
    }

    /**
     * Is custom field used in at least one defined segment?
     */
    public function isFieldUsed(LeadField $field): bool
    {
        return 0 < $this->getFieldSegments($field)->count();
    }

    public function getFieldSegments(LeadField $field)
    {
        $alias       = $field->getAlias();
        $aliasLength = mb_strlen($alias);
        $likeContent = "%;s:5:\"field\";s:{$aliasLength}:\"{$alias}\";%";
        $filter      = [
            'force'  => [
                ['column' => 'l.filters', 'expr' => 'LIKE', 'value'=> $likeContent],
            ],
        ];

        return $this->getEntities(['filter' => $filter]);
    }

    /**
     * @param $segmentId *
     */
    public function getSegmentsWithDependenciesOnSegment($segmentId, $returnProperty = 'name'): array
    {
        $filter = [
            'force'  => [
                ['column' => 'l.filters', 'expr' => 'LIKE', 'value'=>'%s:8:"leadlist"%'],
                ['column' => 'l.id', 'expr' => 'neq', 'value'=>$segmentId],
            ],
        ];
        $entities = $this->getEntities(
            [
                'filter' => $filter,
            ]
        );
        $dependents = [];
        $accessor   = PropertyAccess::createPropertyAccessor();
        foreach ($entities as $entity) {
            $retrFilters = $entity->getFilters();
            foreach ($retrFilters as $eachFilter) {
                // BC support for old filters where the field existed outside of properties.
                $filter = $eachFilter['properties']['filter'] ?? $eachFilter['filter'];
                if ($filter && 'leadlist' === $eachFilter['type'] && in_array($segmentId, $filter)) {
                    if ($returnProperty && $value = $accessor->getValue($entity, $returnProperty)) {
                        $dependents[] = $value;
                    } else {
                        $dependents[] = $entity;
                    }
                    break;
                }
            }
        }

        return $dependents;
    }

    /**
     * @return array<int, int>
     */
    public function getSegmentIdsWithDependenciesOnEmail(int $emailId): array
    {
        $entities = $this->getEntities(
            [
                'filter' => [
                    'force'  => [
                        [
                            'column' => 'l.filters',
                            'expr'   => 'LIKE',
                            'value'  => '%"lead_email_%',
                        ],
                    ],
                ],
            ]
        );

        $emailFilterTypes = ['lead_email_received', 'lead_email_sent'];

        $dependents = [];
        foreach ($entities as $entity) {
            foreach ($entity->getFilters() as $entityFilter) {
                // BC support for old filters where the field existed outside of properties.
                $filter = $entityFilter['properties']['filter'] ?? $entityFilter['filter'];
                if ($filter && in_array($entityFilter['type'], $emailFilterTypes) && in_array($emailId, $filter)) {
                    $dependents[] = $entity->getId();
                    break;
                }
            }
        }

        return array_unique($dependents);
    }

    /**
     * @return array<int, int>
     */
    public function getSegmentIdsWithDependenciesOnTag(int $tagId): array
    {
        $entities = $this->getEntities(
            [
                'filter' => [
                    'force'  => [
                        [
                            'column' => 'l.filters',
                            'expr'   => 'LIKE',
                            'value'  => '%"tags"%',
                        ],
                    ],
                ],
            ]
        );

        $dependents = [];
        foreach ($entities as $entity) {
            foreach ($entity->getFilters() as $entityFilter) {
                // BC support for old filters where the field existed outside of properties.
                $filter = $entityFilter['properties']['filter'] ?? $entityFilter['filter'];
                if ($filter && 'tags' === $entityFilter['type'] && in_array($tagId, $filter)) {
                    $dependents[] = $entity->getId();
                    break;
                }
            }
        }

        return array_unique($dependents);
    }

    /**
     * Get segments which are used as a dependent by other segments to prevent batch deletion of them.
     *
     * @param array $segmentIds
     */
    public function canNotBeDeleted($segmentIds): array
    {
        $entities = $this->getEntities(
            [
                'filter' => [
                    'force'  => [
                        ['column' => 'l.filters', 'expr' => 'LIKE', 'value'=>'%s:8:"leadlist"%'],
                    ],
                ],
            ]
        );

        $idsNotToBeDeleted   = [];
        $namesNotToBeDeleted = [];
        $dependency          = [];

        foreach ($entities as $entity) {
            $retrFilters = $entity->getFilters();
            foreach ($retrFilters as $eachFilter) {
                if ('leadlist' !== $eachFilter['type']) {
                    continue;
                }

                $bcFilterValue     = $eachFilter['filter'] ?? [];
                $filterValue       = $eachFilter['properties']['filter'] ?? $bcFilterValue;
                $idsNotToBeDeleted = array_unique(array_merge($idsNotToBeDeleted, $filterValue));
                foreach ($filterValue as $val) {
                    if (!empty($dependency[$val])) {
                        $dependency[$val] = array_merge($dependency[$val], [$entity->getId()]);
                        $dependency[$val] = array_unique($dependency[$val]);
                    } else {
                        $dependency[$val] = [$entity->getId()];
                    }
                }
            }
        }
        foreach ($dependency as $key => $value) {
            if (array_intersect($value, $segmentIds) === $value) {
                $idsNotToBeDeleted = array_unique(array_diff($idsNotToBeDeleted, [$key]));
            }
        }

        $idsNotToBeDeleted = array_intersect($segmentIds, $idsNotToBeDeleted);

        foreach ($idsNotToBeDeleted as $val) {
            $namesNotToBeDeleted[$val] = $this->getEntity($val)->getName();
        }

        return $namesNotToBeDeleted;
    }

    /**
     * Get a list of source choices.
     */
    public function getSourceLists(?string $sourceType = null): array
    {
        $choices = [];
        switch ($sourceType) {
            case 'categories':
            case null:
                $choices['categories'] = [];
                $categories            = $this->categoryModel->getLookupResults('segment');
                foreach ($categories as $category) {
                    $choices['categories'][$category['alias']] = $category['title'];
                }
        }

        foreach ($choices as &$typeChoices) {
            asort($typeChoices);
        }

        return (null == $sourceType) ? $choices : $choices[$sourceType];
    }

    /**
     * @param array<int> $listIds
     *
     * @return array<int>
     *
     * @throws \Exception
     */
    public function getSegmentContactCountFromCache(array $listIds): array
    {
        $leadCounts = [];

        foreach ($listIds as $listId) {
            $leadCounts[$listId] = $this->segmentCountCacheHelper->getSegmentContactCount($listId);
        }

        return $leadCounts;
    }

    public function leadListExists(int $id): bool
    {
        return $this->getRepository()->leadListExists($id);
    }

    /**
     * @param array<int> $listIds
     *
     * @return array<int>
     *
     * @throws \Exception
     */
    public function getSegmentContactCount(array $listIds): array
    {
        $leadCounts = [];

        foreach ($listIds as $listId) {
            if ($this->segmentCountCacheHelper->hasSegmentContactCount($listId)) {
                $leadCounts[$listId] = $this->segmentCountCacheHelper->getSegmentContactCount($listId);
            } else {
                $count               = $this->getRepository()->getLeadCount($listId);
                $leadCounts[$listId] = $count;
                $this->segmentCountCacheHelper->setSegmentContactCount($listId, $count);
            }
        }

        return $leadCounts;
    }

    public function getActiveSegmentContactCount(int $segmentId): int
    {
        $total = $this->getRepository()->getLeadCount($segmentId);
        $dnc   = $this->doNotContactRepository->getCount(null, null, null, $segmentId);

        return max(0, $total - $dnc);
    }

    /**
     * @param array<int,int> $segmentsFilter
     *
     * @return array<int,LeadList>
     */
    public function getSegmentsBuildTime(int $limit = 10, string $order = 'DESC', array $segmentsFilter = [], bool $canViewOthers = true): array
    {
        $criteria = ['isPublished' => true];

        if (!$canViewOthers) {
            $criteria['createdBy'] = $this->userHelper->getUser()->getId();
        }

        if (!empty($segmentsFilter)) {
            $criteria['id'] = $segmentsFilter;
        }

        return $this->getRepository()->findBy($criteria, ['lastBuiltTime' => $order], $limit);
    }
}
