<?php

namespace Mautic\LeadBundle\Event;

use Mautic\CoreBundle\Helper\Chart\ChartQuery;
use Mautic\CoreBundle\Helper\DateTimeHelper;
use Mautic\LeadBundle\Entity\Lead;
use Symfony\Contracts\EventDispatcher\Event;

class LeadTimelineEvent extends Event
{
    /**
     * Container with all filtered events.
     *
     * @var array
     */
    protected $events = [];

    /**
     * Container with all registered events types.
     *
     * @var array
     */
    protected $eventTypes = [];

    /**
     * Array of filters
     *  search => (string) search term
     *  includeEvents => (array) event types to include
     *  excludeEvents => (array) event types to exclude.
     *
     * @var array
     */
    protected $filters = [];

    /**
     * @var array<string, int>
     */
    protected $totalEvents = [];

    /**
     * @var array
     */
    protected $totalEventsByUnit = [];

    /**
     * @var bool
     */
    protected $countOnly = false;

    /**
     * @var \DateTimeInterface|null
     */
    protected $dateFrom;

    /**
     * @var \DateTimeInterface|null
     */
    protected $dateTo;

    /**
     * Time unit to group counts by (M = month, D = day, Y = year, null = no grouping).
     *
     * @var string
     */
    protected $groupUnit;

    /**
     * @var ChartQuery
     */
    protected $chartQuery;

    /**
     * @var bool
     */
    protected $fetchTypesOnly = false;

    /**
     * @var array
     */
    protected $serializerGroups = [
        'ipAddressList',
    ];

    /**
     * @param Lead|null   $lead        Lead entity for the lead the timeline is being generated for
     * @param int         $page
     * @param int         $limit       Limit per type
     * @param bool        $forTimeline
     * @param string|null $siteDomain
     */
    public function __construct(
        protected ?Lead $lead = null,
        array $filters = [],
        protected ?array $orderBy = null,
        protected $page = 1,
        protected $limit = 25,
        protected $forTimeline = true,
        protected $siteDomain = null,
    ) {
        $this->filters = !empty($filters)
            ? $filters
            :
            [
                'search'        => '',
                'includeEvents' => [],
                'excludeEvents' => [],
            ];

        if (!empty($filters['dateFrom'])) {
            $this->dateFrom = ($filters['dateFrom'] instanceof \DateTime) ? $filters['dateFrom'] : new \DateTime($filters['dateFrom']);
        }

        if (!empty($filters['dateTo'])) {
            $this->dateTo = ($filters['dateTo'] instanceof \DateTime) ? $filters['dateTo'] : new \DateTime($filters['dateTo']);
        }
    }

    /**
     * Add an event to the container.
     *
     * The data should be an associative array with the following data:
     * 'event'     => string    The event name
     * 'timestamp' => \DateTime The timestamp of the event
     * 'extra'     => array     An optional array of extra data for the event
     *
     * @param array $data Data array for the table
     */
    public function addEvent(array $data): void
    {
        if ($this->countOnly) {
            // BC support for old format
            if ($this->groupUnit && $this->chartQuery) {
                $countData = [
                    [
                        'date'  => $data['timestamp'],
                        'count' => 1,
                    ],
                ];

                $count = $this->chartQuery->completeTimeData($countData);
                $this->addToCounter($data['event'], $count);
            } else {
                if (!isset($this->totalEvents[$data['event']])) {
                    $this->totalEvents[$data['event']] = 0;
                }
                ++$this->totalEvents[$data['event']];
            }
        } else {
            if (!isset($this->events[$data['event']])) {
                $this->events[$data['event']] = [];
            }

            if (!$this->isForTimeline()) {
                // standardize the payload
                $keepThese = [
                    'event'      => true,
                    'eventId'    => true,
                    'eventLabel' => true,
                    'eventType'  => true,
                    'timestamp'  => true,
                    'contactId'  => true,
                    'extra'      => true,
                ];

                $data = array_intersect_key($data, $keepThese);

                // Rename extra to details
                if (isset($data['extra'])) {
                    $data['details'] = $data['extra'];
                    $data['details'] = $this->prepareDetailsForAPI($data['details']);
                    unset($data['extra']);
                }

                // Ensure a full URL
                if ($this->siteDomain && isset($data['eventLabel']) && is_array($data['eventLabel']) && isset($data['eventLabel']['href'])) {
                    // If this does not have a http, then assume a Mautic URL
                    if (!str_contains($data['eventLabel']['href'], '://')) {
                        $data['eventLabel']['href'] = $this->siteDomain.$data['eventLabel']['href'];
                    }
                }
            }

            if (empty($data['eventId'])) {
                // Every entry should have an eventId so generate one if the listener itself didn't handle this
                $data['eventId'] = $this->generateEventId($data);
            }

            $this->events[$data['event']][] = $data;
        }
    }

    /**
     * Fetch the events.
     *
     * @return array Events sorted by timestamp with most recent event first
     */
    public function getEvents()
    {
        if (empty($this->events)) {
            return [];
        }

        $events = call_user_func_array('array_merge', array_values($this->events));

        foreach ($events as &$e) {
            if (!$e['timestamp'] instanceof \DateTime) {
                $dt             = new DateTimeHelper($e['timestamp'], 'Y-m-d H:i:s', 'UTC');
                $e['timestamp'] = $dt->getDateTime();
                unset($dt);
            }
        }

        if (!empty($this->orderBy)) {
            usort(
                $events,
                function ($a, $b) {
                    switch ($this->orderBy[0]) {
                        case 'eventLabel':
                            $aLabel = '';
                            if (isset($a['eventLabel'])) {
                                $aLabel = (is_array($a['eventLabel'])) ? $a['eventLabel']['label'] : $a['eventLabel'];
                            }

                            $bLabel = '';
                            if (isset($b['eventLabel'])) {
                                $bLabel = (is_array($b['eventLabel'])) ? $b['eventLabel']['label'] : $b['eventLabel'];
                            }

                            return strnatcmp($aLabel, $bLabel);

                        case 'timestamp':
                            if ($a['timestamp'] == $b['timestamp']) {
                                $aPriority = isset($a['eventPriority']) ? (int) $a['eventPriority'] : 0;
                                $bPriority = isset($b['eventPriority']) ? (int) $b['eventPriority'] : 0;

                                return $aPriority - $bPriority;
                            }

                            return $a['timestamp'] < $b['timestamp'] ? -1 : 1;
                    }
                }
            );

            if ('DESC' == $this->orderBy[1]) {
                $events = array_reverse($events);
            }
        }

        return $events;
    }

    /**
     * Get the max number of pages for pagination.
     *
     * @return float|int
     */
    public function getMaxPage()
    {
        if (!$this->totalEvents) {
            return 1;
        }

        // Find the type that has the largest number of total records
        $largest = max($this->totalEvents);

        // Max page is $largest / $limit
        return ($largest) ? ceil($largest / $this->limit) : 1;
    }

    /**
     * Add an event type to the container.
     *
     * @param string $eventTypeKey  Identifier of the event type
     * @param string $eventTypeName Name of the event type for humans
     */
    public function addEventType($eventTypeKey, $eventTypeName): void
    {
        $this->eventTypes[$eventTypeKey] = $eventTypeName;
    }

    /**
     * Fetch the event types.
     *
     * @return array of available types
     */
    public function getEventTypes()
    {
        natcasesort($this->eventTypes);

        return $this->eventTypes;
    }

    /**
     * Fetch the filter array for queries.
     *
     * @return array of wanted filteres. Empty == all
     */
    public function getEventFilters()
    {
        return $this->filters['search'];
    }

    /**
     * Fetch the order for queries.
     *
     * @return array|null
     */
    public function getEventOrder()
    {
        return $this->orderBy;
    }

    /**
     * Fetch start/limit for queries.
     */
    public function getEventLimit(): array
    {
        return [
            'leadId' => ($this->lead instanceof Lead) ? $this->lead->getId() : null,
            'limit'  => $this->limit,
            'start'  => (1 >= $this->page) ? 0 : ($this->page - 1) * $this->limit,
        ];
    }

    public function getQueryOptions(): array
    {
        return array_merge(
            [
                'search'     => $this->filters['search'],
                'order'      => $this->orderBy,
                'paginated'  => !$this->countOnly,
                'unitCounts' => $this->countOnly && $this->groupUnit,
                'unit'       => $this->groupUnit,
                'fromDate'   => $this->dateFrom,
                'toDate'     => $this->dateTo,
                'chartQuery' => $this->chartQuery,
            ],
            $this->getEventLimit()
        );
    }

    /**
     * Fetches the lead being acted on.
     *
     * @return Lead
     */
    public function getLead()
    {
        return $this->lead;
    }

    /**
     * Returns the lead ID if any.
     */
    public function getLeadId(): ?int
    {
        return ($this->lead instanceof Lead) ? $this->lead->getId() : null;
    }

    /**
     * Determine if an event type should be included.
     *
     * @param bool $inclusive
     */
    public function isApplicable($eventType, $inclusive = false): bool
    {
        if ($this->fetchTypesOnly) {
            return false;
        }

        if (in_array($eventType, $this->filters['excludeEvents'])) {
            return false;
        }

        if (!empty($this->filters['includeEvents'])) {
            if (!in_array($eventType, $this->filters['includeEvents'])) {
                return false;
            }
        } elseif ($inclusive) {
            return false;
        }

        return true;
    }

    /**
     * Check if the event is getting an engagement count only.
     *
     * @return bool
     */
    public function isEngagementCount()
    {
        return $this->countOnly;
    }

    /**
     * Get the date range to get counts by.
     */
    public function getCountDateRange(): array
    {
        return ['from' => $this->dateFrom, 'to' => $this->dateTo];
    }

    /**
     * Get the unit counts are to be grouped by.
     *
     * @return string
     */
    public function getCountGroupingUnit()
    {
        return $this->groupUnit;
    }

    /**
     * Get total number of events for pagination.
     *
     * @return mixed[]
     */
    public function getEventCounter(): array
    {
        // BC support for old formats
        foreach ($this->events as $type => $events) {
            if (!isset($this->totalEvents[$type])) {
                $this->totalEvents[$type] = count($events);
            }
        }

        $counter = [
            'total' => array_sum($this->totalEvents),
        ];

        if ($this->countOnly && $this->groupUnit) {
            $counter['byUnit'] = $this->totalEventsByUnit;
        }

        return $counter;
    }

    /**
     * Add to the event counters.
     *
     * @param int|array $count
     */
    public function addToCounter($eventType, $count): void
    {
        if (!isset($this->totalEvents[$eventType])) {
            $this->totalEvents[$eventType] = 0;
        }

        if (is_array($count)) {
            if (isset($count['total'])) {
                $this->totalEvents[$eventType] += $count['total'];
            } elseif ($this->isEngagementCount() && $this->groupUnit) {
                // Group counts across events by unit
                foreach ($count as $key => $data) {
                    if (!isset($this->totalEventsByUnit[$key])) {
                        $this->totalEventsByUnit[$key] = 0;
                    }
                    $this->totalEventsByUnit[$key] += (int) $data;
                    $this->totalEvents[$eventType] += (int) $data;
                }
            } else {
                $this->totalEvents[$eventType] = array_sum($count);
            }
        } else {
            $this->totalEvents[$eventType] += (int) $count;
        }
    }

    /**
     * Subtract from the total counter if there is an event that was skipped for whatever reason.
     */
    public function subtractFromCounter($eventType, $count = 1): void
    {
        $this->totalEvents[$eventType] -= $count;
    }

    /**
     * Calculate engagement counts only.
     */
    public function setCountOnly(\DateTime $dateFrom, \DateTime $dateTo, $groupUnit = null, ChartQuery $chartQuery = null): void
    {
        $this->countOnly  = true;
        $this->dateFrom   = $dateFrom;
        $this->dateTo     = $dateTo;
        $this->groupUnit  = $groupUnit;
        $this->chartQuery = $chartQuery;
    }

    /**
     * Get chart query helper to format dates.
     *
     * @return ChartQuery
     */
    public function getChartQuery()
    {
        return $this->chartQuery;
    }

    /**
     * Check if the data is to be display for the contact's timeline or used for the API.
     *
     * @return bool
     */
    public function isForTimeline()
    {
        return $this->forTimeline;
    }

    /**
     * Add a serializer group for API formatting.
     */
    public function addSerializerGroup($group): void
    {
        if (is_array($group)) {
            $this->serializerGroups = array_merge(
                $this->serializerGroups,
                $group
            );
        } else {
            $this->serializerGroups[$group] = $group;
        }
    }

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

    /**
     * Will cause isApplicable to return false for all in order to just compile a list of event types.
     */
    public function fetchTypesOnly(): void
    {
        $this->fetchTypesOnly = true;
    }

    /**
     * Convert all snake case keys o camel case for API congruency.
     */
    private function prepareDetailsForAPI(array $details): array
    {
        foreach ($details as $key => &$detailValues) {
            if (is_array($detailValues)) {
                $this->prepareDetailsForAPI($detailValues);
            }

            if ('lead_id' === $key) {
                // Don't include this as it should be included in parent as contactId
                unset($details[$key]);
                continue;
            }

            if (strstr($key, '_')) {
                $newKey           = lcfirst(str_replace('_', '', ucwords($key, '_')));
                $details[$newKey] = $details[$key];
                unset($details[$key]);
            }
        }

        return $details;
    }

    /**
     * Generate something consistent for this event to identify this log entry.
     */
    private function generateEventId(array $data): string
    {
        return $data['eventType'].hash('crc32', json_encode($data), false);
    }
}
