<?php

namespace Mautic\LeadBundle\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping as ORM;
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
use Mautic\CoreBundle\Entity\FormEntity;
use Mautic\CoreBundle\Entity\IpAddress;
use Mautic\CoreBundle\Entity\SkipModifiedInterface;
use Mautic\LeadBundle\DataObject\LeadManipulator;
use Mautic\LeadBundle\Form\Validator\Constraints\UniqueCustomField;
use Mautic\LeadBundle\Model\FieldModel;
use Mautic\NotificationBundle\Entity\PushID;
use Mautic\PointBundle\Entity\Group;
use Mautic\PointBundle\Entity\GroupContactScore;
use Mautic\StageBundle\Entity\Stage;
use Mautic\UserBundle\Entity\User;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Mapping\ClassMetadata;

#[ApiResource(
    shortName: 'Contacts',
    operations: [
        new GetCollection(uriTemplate: '/contacts', security: "is_granted('lead:leads:viewown')"),
        new Post(uriTemplate: '/contacts', security: "is_granted('lead:leads:create')"),
        new Get(uriTemplate: '/contacts/{id}', security: "is_granted('lead:leads:viewown')"),
        new Put(uriTemplate: '/contacts/{id}', security: "is_granted('lead:leads:editown')"),
        new Patch(uriTemplate: '/contacts/{id}', security: "is_granted('lead:leads:editother')"),
        new Delete(uriTemplate: '/contacts/{id}', security: "is_granted('lead:leads:deleteown')"),
    ],
    normalizationContext: [
        'groups'                  => ['contact:read'],
        'swagger_definition_name' => 'Read',
        'api_included'            => ['owner', 'stage', 'tags'],
    ],
    denormalizationContext: [
        'groups'                  => ['contact:write'],
        'swagger_definition_name' => 'Write',
    ]
)]
class Lead extends FormEntity implements CustomFieldEntityInterface, IdentifierFieldEntityInterface, SkipModifiedInterface
{
    use CustomFieldEntityTrait;

    public const FIELD_ALIAS     = '';

    public const POINTS_ADD      = 'plus';

    public const POINTS_SUBTRACT = 'minus';

    public const POINTS_MULTIPLY = 'times';

    public const POINTS_DIVIDE   = 'divide';

    public const POINTS_SET      = 'set';

    public const DEFAULT_ALIAS   = 'l';

    /**
     * Used to determine social identity.
     *
     * @var array
     */
    private $availableSocialFields = [];

    /**
     * @var string
     */
    #[Groups(['contact:read', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $id;

    #[Groups(['contact:read', 'contact:write'])]
    private $title;

    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $firstname;

    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $lastname;

    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $company;

    #[Groups(['contact:read', 'contact:write'])]
    private $position;

    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $email;

    #[Groups(['contact:read', 'contact:write'])]
    private $phone;

    #[Groups(['contact:read', 'contact:write'])]
    private $mobile;

    #[Groups(['contact:read', 'contact:write'])]
    private $address1;

    #[Groups(['contact:read', 'contact:write'])]
    private $address2;

    #[Groups(['contact:read', 'contact:write'])]
    private $city;

    #[Groups(['contact:read', 'contact:write'])]
    private $state;

    #[Groups(['contact:read', 'contact:write'])]
    private $zipcode;

    /**
     * @var string|null
     */
    #[Groups(['contact:read', 'contact:write'])]
    private $timezone;

    #[Groups(['contact:read', 'contact:write'])]
    private $country;

    /**
     * @var User|null
     */
    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $owner;

    /**
     * @var int
     */
    #[Groups(['contact:read', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $points = 0;

    /**
     * @var array
     */
    private $pointChanges = [];

    /**
     * @var int|null
     */
    private $updatedPoints;

    /**
     * @var Collection<int, PointsChangeLog>
     */
    private $pointsChangeLog;

    private ?int $actualPoints = null;

    /**
     * @var Collection<int, CompanyChangeLog>
     */
    private $companyChangeLog;

    /**
     * @var Collection<string, DoNotContact>
     */
    private $doNotContact;

    /**
     * @var Collection<string, IpAddress>
     */
    private $ipAddresses;

    /**
     * @var Collection<int, PushID>
     */
    private $pushIds;

    /**
     * @var ArrayCollection<int, LeadEventLog>
     */
    private $eventLog;

    /**
     * @var \DateTimeInterface
     */
    #[Groups(['contact:read'])]
    private $lastActive;

    /**
     * @var array
     */
    private $internal = [];

    /**
     * @var array
     */
    private $socialCache = [];

    /**
     * Used to populate trigger color.
     *
     * @var string
     */
    private $color;

    /**
     * @var LeadManipulator
     */
    private $manipulator;

    /**
     * @var bool
     */
    private $newlyCreated = false;

    /**
     * @var \DateTimeInterface
     */
    #[Groups(['contact:read'])]
    private $dateIdentified;

    /**
     * @var Collection<int, LeadNote>
     */
    private $notes;

    /**
     * @var string|null
     */
    private $preferredProfileImage = 'gravatar';

    /**
     * @var bool
     */
    public $imported = false;

    /**
     * @var Collection<string, Tag>
     */
    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $tags;

    /**
     * @var Stage|null
     */
    #[Groups(['contact:read', 'contact:write', 'segment:read', 'campaign:read', 'email:read', 'sms:read'])]
    private $stage;

    /**
     * @var Collection<int, StagesChangeLog>
     */
    private $stageChangeLog;

    /**
     * @var Collection<int, UtmTag>
     */
    private $utmtags;

    /**
     * @var Collection<int, FrequencyRule>
     */
    private $frequencyRules;

    /**
     * @var ArrayCollection<int,GroupContactScore>
     */
    private $groupScores;

    private int $previousId = 0;

    private $primaryCompany;

    /**
     * Used to determine order of preferred channels.
     *
     * @var array
     */
    private $channelRules = [];

    public function __construct()
    {
        $this->ipAddresses      = new ArrayCollection();
        $this->pushIds          = new ArrayCollection();
        $this->eventLog         = new ArrayCollection();
        $this->doNotContact     = new ArrayCollection();
        $this->pointsChangeLog  = new ArrayCollection();
        $this->tags             = new ArrayCollection();
        $this->stageChangeLog   = new ArrayCollection();
        $this->frequencyRules   = new ArrayCollection();
        $this->companyChangeLog = new ArrayCollection();
        $this->groupScores      = new ArrayCollection();
    }

    public static function loadMetadata(ORM\ClassMetadata $metadata): void
    {
        $builder = new ClassMetadataBuilder($metadata);

        $builder->setTable('leads')
            ->setCustomRepositoryClass(LeadRepository::class)
            ->addLifecycleEvent('checkDateIdentified', 'preUpdate')
            ->addLifecycleEvent('checkDateIdentified', 'prePersist')
            ->addLifecycleEvent('checkAttributionDate', 'preUpdate')
            ->addLifecycleEvent('checkAttributionDate', 'prePersist')
            ->addLifecycleEvent('checkDateAdded', 'prePersist')
            ->addIndex(['date_added'], 'lead_date_added')
            ->addIndex(['date_modified'], 'lead_date_modified')
            ->addIndex(['date_identified'], 'date_identified');

        $builder->addBigIntIdField();

        $builder->createManyToOne('owner', User::class)
            ->fetchLazy()
            ->addJoinColumn('owner_id', 'id', true, false, 'SET NULL')
            ->build();

        $builder->createField('points', 'integer')
            ->build();

        $builder->createOneToMany('pointsChangeLog', 'PointsChangeLog')
            ->orphanRemoval()
            ->setOrderBy(['dateAdded' => 'DESC'])
            ->mappedBy('lead')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        $builder->createOneToMany('companyChangeLog', 'CompanyChangeLog')
            ->orphanRemoval()
            ->setOrderBy(['dateAdded' => 'DESC'])
            ->mappedBy('lead')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        $builder->createOneToMany('doNotContact', DoNotContact::class)
            ->orphanRemoval()
            ->mappedBy('lead')
            ->cascadePersist()
            ->cascadeDetach()
            ->cascadeMerge()
            ->fetchExtraLazy()
            ->build();

        $builder->createManyToMany('ipAddresses', IpAddress::class)
            ->setJoinTable('lead_ips_xref')
            ->addInverseJoinColumn('ip_id', 'id', true, false, 'CASCADE')
            ->addJoinColumn('lead_id', 'id', false, false, 'CASCADE')
            ->setIndexBy('ipAddress')
            ->cascadeDetach()
            ->cascadeMerge()
            ->cascadePersist()
            ->build();

        $builder->createOneToMany('pushIds', PushID::class)
            ->orphanRemoval()
            ->mappedBy('lead')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        $builder->createOneToMany('eventLog', LeadEventLog::class)
            ->mappedBy('lead')
            ->cascadePersist()
            ->cascadeMerge()
            ->cascadeDetach()
            ->fetchExtraLazy()
            ->build();

        $builder->createField('lastActive', 'datetime')
            ->columnName('last_active')
            ->nullable()
            ->build();

        $builder->createField('internal', 'array')
            ->nullable()
            ->build();

        $builder->createField('socialCache', 'array')
            ->columnName('social_cache')
            ->nullable()
            ->build();

        $builder->createField('dateIdentified', 'datetime')
            ->columnName('date_identified')
            ->nullable()
            ->build();

        $builder->createOneToMany('notes', 'LeadNote')
            ->orphanRemoval()
            ->setOrderBy(['dateAdded' => 'DESC'])
            ->mappedBy('lead')
            ->cascadeDetach()
            ->cascadeMerge()
            ->fetchExtraLazy()
            ->build();

        $builder->createField('preferredProfileImage', 'string')
            ->columnName('preferred_profile_image')
            ->nullable()
            ->build();

        $builder->createManyToMany('tags', Tag::class)
            ->setJoinTable('lead_tags_xref')
            ->addInverseJoinColumn('tag_id', 'id', false)
            ->addJoinColumn('lead_id', 'id', false, false, 'CASCADE')
            ->setOrderBy(['tag' => 'ASC'])
            ->setIndexBy('tag')
            ->fetchLazy()
            ->cascadeMerge()
            ->cascadePersist()
            ->cascadeDetach()
            ->build();

        $builder->createManyToOne('stage', Stage::class)
            ->cascadePersist()
            ->cascadeMerge()
            ->cascadeDetach()
            ->addJoinColumn('stage_id', 'id', true, false, 'SET NULL')
            ->build();

        $builder->createOneToMany('stageChangeLog', 'StagesChangeLog')
            ->orphanRemoval()
            ->setOrderBy(['dateAdded' => 'DESC'])
            ->mappedBy('lead')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        $builder->createOneToMany('utmtags', UtmTag::class)
            ->orphanRemoval()
            ->mappedBy('lead')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        $builder->createOneToMany('frequencyRules', FrequencyRule::class)
            ->orphanRemoval()
            ->setIndexBy('channel')
            ->setOrderBy(['dateAdded' => 'DESC'])
            ->mappedBy('lead')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        $builder->createOneToMany('groupScores', GroupContactScore::class)
            ->mappedBy('contact')
            ->cascadeAll()
            ->fetchExtraLazy()
            ->build();

        self::loadFixedFieldMetadata(
            $builder,
            [
                'title',
                'firstname',
                'lastname',
                'company',
                'position',
                'email',
                'phone',
                'mobile',
                'address1',
                'address2',
                'city',
                'state',
                'zipcode',
                'timezone',
                'country',
            ],
            FieldModel::$coreFields
        );
    }

    /**
     * Prepares the metadata for API usage.
     */
    public static function loadApiMetadata(ApiMetadataDriver $metadata): void
    {
        $metadata->setRoot('lead')
            ->setGroupPrefix('leadBasic')
            ->addListProperties(
                [
                    'id',
                    'points',
                    'color',
                    'title',
                    'firstname',
                    'lastname',
                    'company',
                    'position',
                    'email',
                    'phone',
                    'mobile',
                    'address1',
                    'address2',
                    'city',
                    'state',
                    'zipcode',
                    'timezone',
                    'country',
                ]
            )
            ->setGroupPrefix('lead')
            ->addListProperties(
                [
                    'id',
                    'points',
                    'color',
                    'fields',
                ]
            )
            ->addProperties(
                [
                    'lastActive',
                    'owner',
                    'ipAddresses',
                    'tags',
                    'utmtags',
                    'stage',
                    'dateIdentified',
                    'preferredProfileImage',
                    'doNotContact',
                    'frequencyRules',
                ]
            )
            ->build();
    }

    public static function loadValidatorMetadata(ClassMetadata $metadata): void
    {
        $metadata->addConstraint(new UniqueCustomField(['object' => 'lead']));
    }

    public static function getDefaultIdentifierFields(): array
    {
        return [
            'firstname',
            'lastname',
            'company',
            'email',
        ];
    }

    /**
     * @param string     $prop
     * @param mixed      $val
     * @param mixed|null $oldValue
     */
    protected function isChanged($prop, $val, $oldValue = null)
    {
        $getter  = 'get'.ucfirst($prop);
        $current = $oldValue ?? $this->$getter();
        if ('owner' == $prop) {
            if ($current && !$val) {
                $this->changes['owner'] = [$current->getId(), $val];
            } elseif (!$current && $val) {
                $this->changes['owner'] = [$current, $val->getId()];
            } elseif ($current && $val && $current->getId() != $val->getId()) {
                $this->changes['owner'] = [$current->getId(), $val->getId()];
            }
        } elseif ('ipAddresses' == $prop) {
            $this->changes['ipAddresses'] = ['', $val->getIpAddress()]; // Kept for BC. Not a good way to track changes on a collection

            if (empty($this->changes['ipAddressList'])) {
                $this->changes['ipAddressList'] = [];
            }

            $this->changes['ipAddressList'][$val->getIpAddress()] = $val;
        } elseif ('tags' == $prop) {
            if ($val instanceof Tag) {
                $this->changes['tags']['added'][] = $val->getTag();
            } else {
                $this->changes['tags']['removed'][] = $val;
            }
        } elseif ('utmtags' == $prop) {
            if ($val instanceof UtmTag) {
                if ($val->getUtmContent()) {
                    $this->changes['utmtags'] = ['utm_content', $val->getUtmContent()];
                }
                if ($val->getUtmMedium()) {
                    $this->changes['utmtags'] = ['utm_medium', $val->getUtmMedium()];
                }
                if ($val->getUtmCampaign()) {
                    $this->changes['utmtags'] = ['utm_campaign', $val->getUtmCampaign()];
                }
                if ($val->getUtmTerm()) {
                    $this->changes['utmtags'] = ['utm_term', $val->getUtmTerm()];
                }
                if ($val->getUtmSource()) {
                    $this->changes['utmtags'] = ['utm_source', $val->getUtmSource()];
                }
            }
        } elseif ('frequencyRules' == $prop) {
            if (!isset($this->changes['frequencyRules'])) {
                $this->changes['frequencyRules'] = [];
            }

            if ($val instanceof FrequencyRule) {
                $channel = $val->getChannel();

                $this->changes['frequencyRules'][$channel] = $val->getChanges();
            } else {
                $this->changes['frequencyRules']['removed'][] = $val;
            }
        } elseif ('stage' == $prop) {
            if ($current && !$val) {
                $this->changes['stage'] = [$current->getId(), $val];
            } elseif (!$current && $val) {
                $this->changes['stage'] = [$current, $val->getId()];
            } elseif ($current && $val && $current->getId() != $val->getId()) {
                $this->changes['stage'] = [$current->getId(), $val->getId()];
            }
        } elseif ('points' == $prop && $current != $val) {
            $this->changes['points'] = [$current, $val];
        } else {
            parent::isChanged($prop, $val);
        }
    }

    public function convertToArray(): array
    {
        return get_object_vars($this);
    }

    /**
     * Set id.
     *
     * @param int $id
     *
     * @return Lead
     */
    public function setId($id)
    {
        $this->id = (string) $id;

        return $this;
    }

    /**
     * Get id.
     */
    public function getId(): int
    {
        return (int) $this->id;
    }

    /**
     * Set owner.
     *
     * @return Lead
     */
    public function setOwner(?User $owner = null)
    {
        $this->isChanged('owner', $owner);
        $this->owner = $owner;

        return $this;
    }

    /**
     * @return User|null
     */
    public function getOwner()
    {
        return $this->owner;
    }

    /**
     * Returns the user to be used for permissions.
     *
     * @return User|int
     */
    public function getPermissionUser()
    {
        return $this->getOwner() ?? $this->getCreatedBy();
    }

    public function addIpAddress(IpAddress $ipAddress): self
    {
        if (!$ipAddress->isTrackable()) {
            return $this;
        }

        $ip = $ipAddress->getIpAddress();
        if (null !== $ip && !isset($this->ipAddresses[$ip])) {
            $this->isChanged('ipAddresses', $ipAddress);
            $this->ipAddresses[$ip] = $ipAddress;
        }

        return $this;
    }

    public function removeIpAddress(IpAddress $ipAddress): void
    {
        $this->ipAddresses->removeElement($ipAddress);
    }

    /**
     * @return Collection
     */
    public function getIpAddresses()
    {
        return $this->ipAddresses;
    }

    /**
     * Get full name.
     *
     * @param bool $lastFirst
     *
     * @return string
     */
    public function getName($lastFirst = false)
    {
        $firstName = $this->getFirstname();
        $lastName  = $this->getLastname();

        $fullName = '';
        if ($lastFirst && $firstName && $lastName) {
            $fullName = $lastName.', '.$firstName;
        } elseif ($firstName && $lastName) {
            $fullName = $firstName.' '.$lastName;
        } elseif ($firstName) {
            $fullName = $firstName;
        } elseif ($lastName) {
            $fullName = $lastName;
        }

        return $fullName;
    }

    /**
     * Get preferred locale.
     *
     * @return string
     */
    public function getPreferredLocale()
    {
        if (isset($this->updatedFields['preferred_locale'])) {
            return $this->updatedFields['preferred_locale'];
        }

        if (!empty($this->fields['core']['preferred_locale']['value'])) {
            return $this->fields['core']['preferred_locale']['value'];
        }

        return '';
    }

    /**
     * Get the primary identifier for the lead.
     *
     * @param bool $lastFirst
     *
     * @return string
     */
    public function getPrimaryIdentifier($lastFirst = false)
    {
        if ($name = $this->getName($lastFirst)) {
            return $name;
        } elseif ($this->getCompany()) {
            return $this->getCompany();
        } elseif ($this->getEmail()) {
            return $this->getEmail();
        } elseif ($socialIdentity = $this->getFirstSocialIdentity()) {
            return $socialIdentity;
        } elseif (count($ips = $this->getIpAddresses())) {
            return $ips->first()->getIpAddress();
        } else {
            return 'mautic.lead.lead.anonymous';
        }
    }

    /**
     * Get the secondary identifier for the lead; mainly company.
     *
     * @return string
     */
    public function getSecondaryIdentifier()
    {
        if ($this->getCompany()) {
            return $this->getCompany();
        }

        return '';
    }

    /**
     * Get the location for the lead.
     */
    public function getLocation(): string
    {
        $location = '';

        if ($this->getCity()) {
            $location .= $this->getCity().', ';
        }

        if ($this->getState()) {
            $location .= $this->getState().', ';
        }

        if ($this->getCountry()) {
            $location .= $this->getCountry().', ';
        }

        return rtrim($location, ', ');
    }

    /**
     * Point changes are tracked and will be persisted as a direct DB query to avoid PHP memory overwrites with concurrent requests
     * The risk in this is that the $changes['points'] may not be accurate but at least no points are lost.
     *
     * @param int    $points
     * @param string $operator
     *
     * @return Lead
     */
    public function adjustPoints($points, $operator = self::POINTS_ADD)
    {
        if (!$points = (int) $points) {
            return $this;
        }

        // Use $updatedPoints in an attempt to keep track in the $changes log although this may not be accurate if the DB updates the points rather
        // than PHP memory
        if (null == $this->updatedPoints) {
            $this->updatedPoints = $this->points;
        }
        $oldPoints = $this->updatedPoints;

        switch ($operator) {
            case self::POINTS_ADD:
                $this->updatedPoints += $points;
                $operator = '+';
                break;
            case self::POINTS_SUBTRACT:
                $this->updatedPoints -= $points;
                $operator = '-';
                break;
            case self::POINTS_MULTIPLY:
                $this->updatedPoints *= $points;
                $operator = '*';
                break;
            case self::POINTS_DIVIDE:
                $this->updatedPoints /= $points;
                $operator = '/';
                break;
            default:
                throw new \UnexpectedValueException('Invalid operator');
        }

        // Keep track of point changes to make a direct DB query
        // Ignoring Aunt Sally here (PEMDAS)
        if (!isset($this->pointChanges[$operator])) {
            $this->pointChanges[$operator] = 0;
        }
        $this->pointChanges[$operator] += $points;

        $this->isChanged('points', (int) $this->updatedPoints, (int) $oldPoints);

        return $this;
    }

    /**
     * @return array
     */
    public function getPointChanges()
    {
        return $this->pointChanges;
    }

    /**
     * Set points.
     *
     * @param int $points
     *
     * @return Lead
     */
    public function setPoints($points)
    {
        $this->isChanged('points', $points);
        $this->points = (int) $points;

        // Something is setting points directly so reset points updated by database
        $this->resetPointChanges();

        return $this;
    }

    /**
     * Get points.
     *
     * @return int
     */
    public function getPoints()
    {
        if (null !== $this->actualPoints) {
            return $this->actualPoints;
        } elseif (null !== $this->updatedPoints) {
            return $this->updatedPoints;
        }

        return $this->points;
    }

    /**
     * Set by the repository method when points are updated and requeried directly on the DB side.
     */
    public function setActualPoints($points): void
    {
        $this->actualPoints = (int) $points;
        $this->pointChanges = [];
    }

    /**
     * Reset point changes.
     *
     * @return $this
     */
    public function resetPointChanges()
    {
        $this->actualPoints  = null;
        $this->pointChanges  = [];
        $this->updatedPoints = null;

        return $this;
    }

    /**
     * Creates a points change entry.
     */
    public function addPointsChangeLogEntry(string $type, string $name, string $action, int $pointChanges, IpAddress $ip, ?Group $group = null): void
    {
        if (0 === $pointChanges) {
            // No need to record no change
            return;
        }

        // Create a new points change event
        $event = new PointsChangeLog();
        $event->setType($type);
        $event->setEventName($name);
        $event->setActionName($action);
        $event->setDateAdded(new \DateTime());
        $event->setDelta($pointChanges);
        $event->setIpAddress($ip);
        $event->setLead($this);
        if ($group) {
            $event->setGroup($group);
        }
        $this->addPointsChangeLog($event);
    }

    /**
     * Add pointsChangeLog.
     *
     * @return Lead
     */
    public function addPointsChangeLog(PointsChangeLog $pointsChangeLog)
    {
        $this->pointsChangeLog[] = $pointsChangeLog;

        return $this;
    }

    /**
     * Creates a points change entry.
     */
    public function stageChangeLogEntry($stage, $name, $action): void
    {
        // create a new points change event
        $event = new StagesChangeLog();
        $event->setStage($stage);
        $event->setEventName($name);
        $event->setActionName($action);
        $event->setDateAdded(new \DateTime());
        $event->setLead($this);
        $this->stageChangeLog($event);
    }

    /**
     * Add StagesChangeLog.
     *
     * @return Lead
     */
    public function stageChangeLog(StagesChangeLog $stageChangeLog)
    {
        $this->stageChangeLog[] = $stageChangeLog;

        return $this;
    }

    /**
     * @return Collection<int, StagesChangeLog>
     */
    public function getStageChangeLog()
    {
        return $this->stageChangeLog;
    }

    /**
     * Remove pointsChangeLog.
     */
    public function removePointsChangeLog(PointsChangeLog $pointsChangeLog): void
    {
        $this->pointsChangeLog->removeElement($pointsChangeLog);
    }

    /**
     * Get pointsChangeLog.
     *
     * @return Collection
     */
    public function getPointsChangeLog()
    {
        return $this->pointsChangeLog;
    }

    public function addCompanyChangeLogEntry($type, $name, $action, $company = null): ?CompanyChangeLog
    {
        if (!$company) {
            // No need to record a null delta
            return null;
        }

        // Create a new company change event
        $event = new CompanyChangeLog();
        $event->setType($type);
        $event->setEventName($name);
        $event->setActionName($action);
        $event->setDateAdded(new \DateTime());
        $event->setCompany($company);
        $event->setLead($this);
        $this->addCompanyChangeLog($event);

        return $event;
    }

    /**
     * Add Company ChangeLog.
     *
     * @return Lead
     */
    public function addCompanyChangeLog(CompanyChangeLog $companyChangeLog)
    {
        $this->companyChangeLog[] = $companyChangeLog;

        return $this;
    }

    /**
     * @return ArrayCollection<int,CompanyChangeLog>
     */
    public function getCompanyChangeLog()
    {
        return $this->companyChangeLog;
    }

    /**
     * @param bool $enabled
     * @param bool $mobile
     *
     * @return $this
     */
    public function addPushIDEntry($identifier, $enabled = true, $mobile = false)
    {
        $entity = new PushID();

        /** @var PushID $id */
        foreach ($this->pushIds as $id) {
            if ($id->getPushID() === $identifier) {
                if ($id->isEnabled() === $enabled) {
                    return $this;
                } else {
                    $entity = $id;
                    $this->removePushID($id);
                }
            }
        }

        $entity->setPushID($identifier);
        $entity->setLead($this);
        $entity->setEnabled($enabled);
        $entity->setMobile($mobile);

        $this->addPushID($entity);

        $this->isChanged('pushIds', $this->pushIds);

        return $this;
    }

    /**
     * @return $this
     */
    public function addPushID(PushID $pushID)
    {
        $this->pushIds[] = $pushID;

        return $this;
    }

    public function removePushID(PushID $pushID): void
    {
        $this->pushIds->removeElement($pushID);
    }

    /**
     * @return Collection<int, PushID>
     */
    public function getPushIDs()
    {
        return $this->pushIds;
    }

    /**
     * @return $this
     */
    public function addEventLog(LeadEventLog $log)
    {
        $this->eventLog[] = $log;
        $log->setLead($this);

        return $this;
    }

    public function removeEventLog(LeadEventLog $eventLog): void
    {
        $this->eventLog->removeElement($eventLog);
    }

    public function getLastEventLogByAction(string $action): ?LeadEventLog
    {
        $criteria = Criteria::create()
            ->where(Criteria::expr()->eq('action', $action))
            ->orderBy(['dateAdded' => Order::Descending->value])
            ->setFirstResult(0)
            ->setMaxResults(1);

        return $this->eventLog->matching($criteria)->first() ?: null;
    }

    /**
     * @return $this
     */
    public function addDoNotContactEntry(DoNotContact $doNotContact)
    {
        $this->changes['dnc_channel_status'][$doNotContact->getChannel()] = [
            'reason'   => $doNotContact->getReason(),
            'comments' => $doNotContact->getComments(),
        ];

        $this->doNotContact[$doNotContact->getChannel()] = $doNotContact;

        return $this;
    }

    public function removeDoNotContactEntry(DoNotContact $doNotContact): void
    {
        $this->changes['dnc_channel_status'][$doNotContact->getChannel()] = [
            'reason'     => DoNotContact::IS_CONTACTABLE,
            'old_reason' => $doNotContact->getReason(),
            'comments'   => $doNotContact->getComments(),
        ];

        $this->doNotContact->removeElement($doNotContact);
    }

    /**
     * @return Collection<string, DoNotContact>
     */
    public function getDoNotContact(): Collection
    {
        return $this->doNotContact;
    }

    /**
     * Set internal storage.
     */
    public function setInternal($internal): void
    {
        $this->internal = $internal;
    }

    /**
     * Get internal storage.
     *
     * @return mixed
     */
    public function getInternal()
    {
        return $this->internal;
    }

    /**
     * Set social cache.
     */
    public function setSocialCache($cache): void
    {
        $this->socialCache = $cache;
    }

    /**
     * Get social cache.
     *
     * @return mixed
     */
    public function getSocialCache()
    {
        return $this->socialCache;
    }

    /**
     * @return mixed
     */
    public function getColor()
    {
        return $this->color;
    }

    /**
     * @param mixed $color
     */
    public function setColor($color): void
    {
        $this->color = $color;
    }

    public function isAnonymous(): bool
    {
        return !($this->getName()
            || $this->getFirstname()
            || $this->getLastname()
            || $this->getCompany()
            || $this->getEmail()
            || $this->getFirstSocialIdentity()
        );
    }

    public function wasAnonymous(): bool
    {
        return null == $this->dateIdentified && false === $this->isAnonymous();
    }

    /**
     * @return bool
     */
    protected function getFirstSocialIdentity()
    {
        if (isset($this->fields['social'])) {
            foreach ($this->fields['social'] as $social) {
                if (!empty($social['value'])) {
                    return $social['value'];
                }
            }
        } elseif (!empty($this->updatedFields)) {
            foreach ($this->availableSocialFields as $social) {
                if (!empty($this->updatedFields[$social])) {
                    return $this->updatedFields[$social];
                }
            }
        }

        return false;
    }

    /**
     * @return self
     */
    public function setManipulator(?LeadManipulator $manipulator = null)
    {
        $this->manipulator = $manipulator;

        return $this;
    }

    /**
     * @return LeadManipulator|null
     */
    public function getManipulator()
    {
        return $this->manipulator;
    }

    /**
     * @return bool
     */
    public function isNewlyCreated()
    {
        return $this->newlyCreated;
    }

    /**
     * @param bool $newlyCreated Created
     */
    public function setNewlyCreated($newlyCreated): void
    {
        $this->newlyCreated = $newlyCreated;
    }

    /**
     * @return mixed
     */
    public function getNotes()
    {
        return $this->notes;
    }

    /**
     * @param string $source
     */
    public function setPreferredProfileImage($source): void
    {
        $this->preferredProfileImage = $source;
    }

    /**
     * @return string
     */
    public function getPreferredProfileImage()
    {
        return $this->preferredProfileImage;
    }

    /**
     * @return mixed
     */
    public function getDateIdentified()
    {
        return $this->dateIdentified;
    }

    /**
     * @param mixed $dateIdentified
     */
    public function setDateIdentified($dateIdentified): void
    {
        $this->isChanged('dateIdentified', $dateIdentified);
        $this->dateIdentified = $dateIdentified;
    }

    /**
     * @return mixed
     */
    public function getLastActive()
    {
        return $this->lastActive;
    }

    /**
     * @param mixed $lastActive
     */
    public function setLastActive($lastActive): void
    {
        $this->changes['dateLastActive'] = [$this->lastActive, $lastActive];
        $this->lastActive                = $lastActive;
    }

    public function setAvailableSocialFields(array $availableSocialFields): void
    {
        $this->availableSocialFields = $availableSocialFields;
    }

    /**
     * Add tag.
     *
     * @return Lead
     */
    public function addTag(Tag $tag)
    {
        $this->isChanged('tags', $tag);
        $this->tags[$tag->getTag()] = $tag;

        return $this;
    }

    /**
     * Remove tag.
     */
    public function removeTag(Tag $tag): void
    {
        $this->isChanged('tags', $tag->getTag());
        $this->tags->removeElement($tag);
    }

    /**
     * Get tags.
     *
     * @return mixed
     */
    public function getTags()
    {
        return $this->tags;
    }

    /**
     * Set tags.
     *
     * @return $this
     */
    public function setTags($tags)
    {
        $this->tags = $tags;

        return $this;
    }

    /**
     * Get utm tags.
     *
     * @return mixed
     */
    public function getUtmTags()
    {
        return $this->utmtags;
    }

    /**
     * Set utm tags.
     *
     * @return $this
     */
    public function setUtmTags($utmTags)
    {
        $this->isChanged('utmtags', $utmTags);
        $this->utmtags[] = $utmTags;

        return $this;
    }

    public function removeUtmTagEntry(UtmTag $utmTag): void
    {
        $this->changes['utmtags'] = ['removed', 'UtmTagID:'.$utmTag->getId()];
        $this->utmtags->removeElement($utmTag);
    }

    /**
     * Set stage.
     *
     * @return Stage
     */
    public function setStage(?Stage $stage = null)
    {
        $this->isChanged('stage', $stage);
        $this->stage = $stage;

        return $this;
    }

    /**
     * Get stage.
     *
     * @return Stage|null
     */
    public function getStage()
    {
        return $this->stage;
    }

    /**
     * Set frequency rules.
     *
     * @param FrequencyRule[] $frequencyRules
     *
     * @return Lead
     */
    public function setFrequencyRules($frequencyRules)
    {
        $this->frequencyRules = new ArrayCollection($frequencyRules);

        return $this;
    }

    /**
     * Get frequency rules.
     *
     * @return Collection<int, FrequencyRule>
     */
    public function getFrequencyRules()
    {
        return $this->frequencyRules;
    }

    /**
     * Remove frequencyRule.
     */
    public function removeFrequencyRule(FrequencyRule $frequencyRule): void
    {
        $this->isChanged('frequencyRules', $frequencyRule->getId(), false);
        $this->frequencyRules->removeElement($frequencyRule);
    }

    /**
     * Add frequency rule.
     */
    public function addFrequencyRule(FrequencyRule $frequencyRule): void
    {
        $this->isChanged('frequencyRules', $frequencyRule, false);
        $this->frequencyRules[] = $frequencyRule;
    }

    /**
     * Get attribution value.
     */
    public function getAttribution(): float
    {
        return (float) $this->getFieldValue('attribution');
    }

    /**
     * If there is an attribution amount but no date, insert today's date.
     */
    public function checkAttributionDate(): void
    {
        $attribution     = $this->getFieldValue('attribution');
        $attributionDate = $this->getFieldValue('attribution_date');

        if (!empty($attribution) && empty($attributionDate)) {
            $this->addUpdatedField('attribution_date', (new \DateTime())->format('Y-m-d'));
        } elseif (empty($attribution) && !empty($attributionDate)) {
            $this->addUpdatedField('attribution_date', null);
        }
    }

    /**
     * Set date identified.
     */
    public function checkDateIdentified(): void
    {
        if ($this->wasAnonymous()) {
            $this->dateIdentified            = new \DateTime();
            $this->changes['dateIdentified'] = ['', $this->dateIdentified];
        }
    }

    /**
     * Set date added if not already set.
     */
    public function checkDateAdded(): void
    {
        if (null === $this->getDateAdded()) {
            $this->setDateAdded(new \DateTime());
        }
    }

    /**
     * @return mixed
     */
    public function getPrimaryCompany()
    {
        return $this->primaryCompany;
    }

    /**
     * @param mixed $primaryCompany
     *
     * @return Lead
     */
    public function setPrimaryCompany($primaryCompany)
    {
        $this->primaryCompany = $primaryCompany;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param mixed $title
     *
     * @return Lead
     */
    public function setTitle($title)
    {
        $this->isChanged('title', $title);
        $this->title = $title;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getFirstname()
    {
        return $this->firstname;
    }

    /**
     * @param mixed $firstname
     *
     * @return Lead
     */
    public function setFirstname($firstname)
    {
        $this->isChanged('firstname', $firstname);
        $this->firstname = $firstname;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getLastname()
    {
        return $this->lastname;
    }

    /**
     * @param mixed $lastname
     *
     * @return Lead
     */
    public function setLastname($lastname)
    {
        $this->isChanged('lastname', $lastname);
        $this->lastname = $lastname;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getPosition()
    {
        return $this->position;
    }

    /**
     * @param mixed $position
     *
     * @return Lead
     */
    public function setPosition($position)
    {
        $this->isChanged('position', $position);
        $this->position = $position;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getPhone()
    {
        return $this->phone;
    }

    /**
     * @param mixed $phone
     *
     * @return Lead
     */
    public function setPhone($phone)
    {
        $this->isChanged('phone', $phone);
        $this->phone = $phone;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getMobile()
    {
        return $this->mobile;
    }

    /**
     * @param mixed $mobile
     *
     * @return Lead
     */
    public function setMobile($mobile)
    {
        $this->isChanged('mobile', $mobile);
        $this->mobile = $mobile;

        return $this;
    }

    /**
     * @return string|null
     */
    public function getLeadPhoneNumber()
    {
        return $this->getMobile() ?: $this->getPhone();
    }

    /**
     * @return mixed
     */
    public function getAddress1()
    {
        return $this->address1;
    }

    /**
     * @param mixed $address1
     *
     * @return Lead
     */
    public function setAddress1($address1)
    {
        $this->isChanged('address1', $address1);
        $this->address1 = $address1;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getAddress2()
    {
        return $this->address2;
    }

    /**
     * @param mixed $address2
     *
     * @return Lead
     */
    public function setAddress2($address2)
    {
        $this->isChanged('address2', $address2);
        $this->address2 = $address2;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getCity()
    {
        return $this->city;
    }

    /**
     * @param mixed $city
     *
     * @return Lead
     */
    public function setCity($city)
    {
        $this->isChanged('city', $city);
        $this->city = $city;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getState()
    {
        return $this->state;
    }

    /**
     * @param mixed $state
     *
     * @return Lead
     */
    public function setState($state)
    {
        $this->isChanged('state', $state);
        $this->state = $state;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getZipcode()
    {
        return $this->zipcode;
    }

    /**
     * @param mixed $zipcode
     *
     * @return Lead
     */
    public function setZipcode($zipcode)
    {
        $this->isChanged('zipcode', $zipcode);
        $this->zipcode = $zipcode;

        return $this;
    }

    /**
     * @return string
     */
    public function getTimezone()
    {
        return $this->timezone;
    }

    /**
     * @param string $timezone
     *
     * @return Lead
     */
    public function setTimezone($timezone)
    {
        $this->isChanged('timezone', $timezone);
        $this->timezone = $timezone;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getCountry()
    {
        return $this->country;
    }

    /**
     * @param mixed $country
     *
     * @return Lead
     */
    public function setCountry($country)
    {
        $this->isChanged('country', $country);
        $this->country = $country;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getCompany()
    {
        return $this->company;
    }

    /**
     * @param mixed $company
     *
     * @return Lead
     */
    public function setCompany($company)
    {
        $this->isChanged('company', $company);
        $this->company = $company;

        return $this;
    }

    /**
     * @return mixed
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * @param mixed $email
     *
     * @return Lead
     */
    public function setEmail($email)
    {
        $this->isChanged('email', $email);
        $this->email = $email;

        return $this;
    }

    /**
     * Returns array of rules with preferred channels first.
     *
     * @return mixed
     */
    public function getChannelRules()
    {
        if (null === $this->channelRules) {
            $frequencyRules = $this->getFrequencyRules()->toArray();
            $dnc            = $this->getDoNotContact();
            $dncChannels    = [];
            /** @var DoNotContact $record */
            foreach ($dnc as $record) {
                $dncChannels[$record->getChannel()] = $record->getReason();
            }

            $this->channelRules = self::generateChannelRules($frequencyRules, $dncChannels);
        }

        return $this->channelRules;
    }

    /**
     * @return $this
     */
    public function setChannelRules(array $rules)
    {
        $this->channelRules = $rules;

        return $this;
    }

    /**
     * Used mostly when batching to generate preferred channels without hydrating associations one at a time.
     *
     * @return array<mixed, array<'dnc'|'frequency', mixed>>
     */
    public static function generateChannelRules(array $frequencyRules, array $dncRules): array
    {
        $rules             = [];
        $dncFrequencyRules = [];
        foreach ($frequencyRules as $rule) {
            if ($rule instanceof FrequencyRule) {
                $ruleArray = [
                    'channel'           => $rule->getChannel(),
                    'pause_from_date'   => $rule->getPauseFromDate(),
                    'pause_to_date'     => $rule->getPauseToDate(),
                    'preferred_channel' => $rule->getPreferredChannel(),
                    'frequency_time'    => $rule->getFrequencyTime(),
                    'frequency_number'  => $rule->getFrequencyNumber(),
                ];

                if (array_key_exists($rule->getChannel(), $dncRules)) {
                    $dncFrequencyRules[$rule->getChannel()] = $ruleArray;
                } else {
                    $rules[$rule->getChannel()] = $ruleArray;
                }
            } else {
                // Already an array
                break;
            }
        }

        if (count($rules)) {
            $frequencyRules = $rules;
        }

        /* @var FrequencyRule $rule */
        usort(
            $frequencyRules,
            function ($a, $b): int {
                if ($a['pause_from_date'] && $a['pause_to_date']) {
                    $now = new \DateTime();
                    if ($now >= $a['pause_from_date'] && $now <= $a['pause_to_date']) {
                        // A is paused so give lower preference
                        return 1;
                    }
                }

                if ($a['preferred_channel'] === $b['preferred_channel']) {
                    if (!$a['frequency_time'] || !$b['frequency_time'] || !$a['frequency_number'] || !$b['frequency_number']) {
                        return 0;
                    }

                    // Order by which ever can be sent more frequent
                    if ($a['frequency_time'] === $b['frequency_time']) {
                        if ($a['frequency_number'] === $b['frequency_number']) {
                            return 0;
                        }

                        return ($a['frequency_number'] > $b['frequency_number']) ? -1 : 1;
                    } else {
                        $convertToMonth = fn ($number, $unit) => match ($unit) {
                            FrequencyRule::TIME_MONTH => (int) $number,
                            FrequencyRule::TIME_WEEK  => $number * 4,
                            FrequencyRule::TIME_DAY   => $number * 30,
                            default                   => $number,
                        };

                        $aFrequency = $convertToMonth($a['frequency_number'], $a['frequency_time']);
                        $bFrequency = $convertToMonth($b['frequency_number'], $b['frequency_time']);

                        return $bFrequency <=> $aFrequency;
                    }
                }

                return ($a['preferred_channel'] > $b['preferred_channel']) ? -1 : 1;
            }
        );

        $rules = [];
        foreach ($frequencyRules as $rule) {
            $rules[$rule['channel']] = [
                'frequency' => $rule,
                'dnc'       => DoNotContact::IS_CONTACTABLE,
            ];
        }

        if (count($dncRules)) {
            foreach ($dncRules as $channel => $reason) {
                $rules[$channel] = [
                    'frequency' => $dncFrequencyRules[$channel] ?? null,
                    'dnc'       => $reason,
                ];
            }
        }

        return $rules;
    }

    public function setPreviousId(int $id): void
    {
        $this->previousId = $id;
    }

    public function getPreviousId(): int
    {
        return $this->previousId;
    }

    /**
     * @return ArrayCollection<int,GroupContactScore>
     */
    public function getGroupScores(): Collection
    {
        return $this->groupScores;
    }

    public function getGroupScore(Group $group): ?GroupContactScore
    {
        foreach ($this->groupScores as $groupScore) {
            if ($groupScore->getGroup() === $group) {
                return $groupScore;
            }
        }

        return null;
    }

    /**
     * @param ArrayCollection<int,GroupContactScore> $groupScores
     */
    public function setGroupScores($groupScores): void
    {
        $this->groupScores = $groupScores;
    }

    public function addGroupScore(GroupContactScore $groupContactScore): Lead
    {
        $this->groupScores[] = $groupContactScore;

        return $this;
    }

    public function removeGroupScore(GroupContactScore $groupContactScore): void
    {
        $this->groupScores->removeElement($groupContactScore);
    }

    /**
     * Do not update modified_by and date_modified fields if only DNC or manipulator was changed.
     * Avoid unnecessary update queries.
     */
    public function shouldSkipSettingModifiedProperties(): bool
    {
        $changes = $this->changes;

        unset($changes['dnc_channel_status']);
        unset($changes['manipulator']);

        return !(bool) count($changes);
    }
}
