<?php

namespace Mautic\CampaignBundle\Executioner\Dispatcher;

use Doctrine\Common\Collections\ArrayCollection;
use Mautic\CampaignBundle\CampaignEvents;
use Mautic\CampaignBundle\Entity\LeadEventLog;
use Mautic\CampaignBundle\Event\CampaignDecisionEvent;
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
use Mautic\CampaignBundle\Event\DecisionEvent;
use Mautic\CampaignBundle\Event\EventArrayTrait;
use Mautic\CampaignBundle\Event\ExecutedBatchEvent;
use Mautic\CampaignBundle\Event\ExecutedEvent;
use Mautic\CampaignBundle\Event\FailedEvent;
use Mautic\CampaignBundle\Event\PendingEvent;
use Mautic\CampaignBundle\EventCollector\Accessor\Event\AbstractEventAccessor;
use Mautic\CampaignBundle\Executioner\Scheduler\EventScheduler;
use Mautic\CoreBundle\Factory\MauticFactory;
use Mautic\LeadBundle\Tracker\ContactTracker;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * @deprecated 2.13.0 to be removed in 3.0; BC support for old listeners
 */
class LegacyEventDispatcher
{
    use EventArrayTrait;

    public function __construct(
        private EventDispatcherInterface $dispatcher,
        private EventScheduler $scheduler,
        private LoggerInterface $logger,
        private MauticFactory $factory,
        private ContactTracker $contactTracker
    ) {
    }

    public function dispatchCustomEvent(
        AbstractEventAccessor $config,
        ArrayCollection $logs,
        $wasBatchProcessed,
        PendingEvent $pendingEvent
    ): void {
        $settings = $config->getConfig();

        if (!isset($settings['eventName']) && !isset($settings['callback'])) {
            // Bad plugin but only fail if the new event didn't already process the log
            if (!$wasBatchProcessed) {
                $pendingEvent->failAll('Invalid event configuration');
            }

            return;
        }

        $rescheduleFailures = new ArrayCollection();

        /** @var LeadEventLog $log */
        foreach ($logs as $log) {
            $this->contactTracker->setSystemContact($log->getLead());

            if (isset($settings['eventName'])) {
                $event  = $this->dispatchEventName($settings['eventName'], $settings, $log);
                $result = $event->getResult();
            } else {
                if (!is_callable($settings['callback'])) {
                    // No use to keep trying for the other logs as it won't ever work
                    break;
                }

                $result = $this->dispatchCallback($settings, $log);
            }

            // If the new batch event was handled, the $log was already processed so only process legacy logs if false
            if (!$wasBatchProcessed) {
                $this->dispatchExecutionEvent($config, $log, $result);

                if (!is_bool($result)) {
                    $log->appendToMetadata($result);
                }

                // Dispatch new events for legacy processed logs
                if ($this->isFailed($result)) {
                    $this->processFailedLog($log, $pendingEvent);

                    $rescheduleFailures->set($log->getId(), $log);

                    $this->dispatchFailedEvent($config, $log);

                    continue;
                }

                if (is_array($result) && !empty($result['failed']) && isset($result['reason'])) {
                    $pendingEvent->passWithError($log, (string) $result['reason']);
                } else {
                    $pendingEvent->pass($log);
                }

                $this->dispatchExecutedEvent($config, $log);
            }
        }

        if ($rescheduleFailures->count()) {
            $this->scheduler->rescheduleFailures($rescheduleFailures);
        }

        $this->contactTracker->setSystemContact(null);
    }

    /**
     * Execute the new ON_EVENT_FAILED and ON_EVENT_EXECUTED events for logs processed by BC code.
     */
    public function dispatchExecutionEvents(AbstractEventAccessor $config, ArrayCollection $success, ArrayCollection $failures): void
    {
        foreach ($success as $log) {
            $this->dispatchExecutionEvent($config, $log, true);
        }

        foreach ($failures as $log) {
            $this->dispatchExecutionEvent($config, $log, false);
        }
    }

    public function dispatchDecisionEvent(DecisionEvent $decisionEvent): void
    {
        if ($this->dispatcher->hasListeners(CampaignEvents::ON_EVENT_DECISION_TRIGGER)) {
            $log   = $decisionEvent->getLog();
            $event = $log->getEvent();

            $legacyDecisionEvent = $this->dispatcher->dispatch(
                new CampaignDecisionEvent(
                    $log->getLead(),
                    $event->getType(),
                    $decisionEvent->getEventConfig()->getConfig(),
                    $this->getLegacyEventsArray($log),
                    $this->getLegacyEventsConfigArray($event, $decisionEvent->getEventConfig()),
                    0 === $event->getOrder(),
                    [$log]
                ),
                CampaignEvents::ON_EVENT_DECISION_TRIGGER
            );

            if ($legacyDecisionEvent->wasDecisionTriggered()) {
                $decisionEvent->setAsApplicable();
            }
        }
    }

    private function dispatchEventName($eventName, array $settings, LeadEventLog $log): CampaignExecutionEvent
    {
        @trigger_error('eventName is deprecated. Convert to using batchEventName.', E_USER_DEPRECATED);

        $campaignEvent = new CampaignExecutionEvent(
            [
                'eventSettings'   => $settings,
                'eventDetails'    => null,
                'event'           => $log->getEvent(),
                'lead'            => $log->getLead(),
                'systemTriggered' => $log->getSystemTriggered(),
            ],
            null,
            $log
        );

        $this->dispatcher->dispatch($campaignEvent, $eventName);

        if ($channel = $campaignEvent->getChannel()) {
            $log->setChannel($channel);
            $log->setChannelId($campaignEvent->getChannelId());
        }

        return $campaignEvent;
    }

    /**
     * @return mixed
     */
    private function dispatchCallback(array $settings, LeadEventLog $log)
    {
        @trigger_error('callback is deprecated. Convert to using batchEventName.', E_USER_DEPRECATED);

        $eventArray = $this->getEventArray($log->getEvent());
        $args       = [
            'eventSettings'   => $settings,
            'eventDetails'    => null, // @todo fix when procesing decisions,
            'event'           => $eventArray,
            'lead'            => $log->getLead(),
            'factory'         => $this->factory,
            'systemTriggered' => $log->getSystemTriggered(),
            'config'          => $eventArray['properties'],
        ];

        try {
            if (is_array($settings['callback'])) {
                $reflection = new \ReflectionMethod($settings['callback'][0], $settings['callback'][1]);
            } elseif (str_contains($settings['callback'], '::')) {
                $parts      = explode('::', $settings['callback']);
                $reflection = new \ReflectionMethod($parts[0], $parts[1]);
            } else {
                $reflection = new \ReflectionMethod(null, $settings['callback']);
            }

            $pass = [];
            foreach ($reflection->getParameters() as $param) {
                if (isset($args[$param->getName()])) {
                    $pass[] = $args[$param->getName()];
                } else {
                    $pass[] = null;
                }
            }

            return $reflection->invokeArgs($this, $pass);
        } catch (\ReflectionException) {
            return false;
        }
    }

    private function dispatchExecutionEvent(AbstractEventAccessor $config, LeadEventLog $log, $result): void
    {
        $eventArray = $this->getEventArray($log->getEvent());

        $this->dispatcher->dispatch(
            new CampaignExecutionEvent(
                [
                    'eventSettings'   => $config->getConfig(),
                    'eventDetails'    => null, // @todo fix when procesing decisions,
                    'event'           => $eventArray,
                    'lead'            => $log->getLead(),
                    'systemTriggered' => $log->getSystemTriggered(),
                    'config'          => $eventArray['properties'],
                ],
                $result,
                $log
            ),
            CampaignEvents::ON_EVENT_EXECUTION
        );
    }

    private function dispatchExecutedEvent(AbstractEventAccessor $config, LeadEventLog $log): void
    {
        $this->dispatcher->dispatch(
            new ExecutedEvent($config, $log),
            CampaignEvents::ON_EVENT_EXECUTED
        );

        $collection = new ArrayCollection();
        $collection->set($log->getId(), $log);
        $this->dispatcher->dispatch(
            new ExecutedBatchEvent($config, $log->getEvent(), $collection),
            CampaignEvents::ON_EVENT_EXECUTED_BATCH
        );
    }

    private function dispatchFailedEvent(AbstractEventAccessor $config, LeadEventLog $log): void
    {
        $this->dispatcher->dispatch(
            new FailedEvent($config, $log),
            CampaignEvents::ON_EVENT_FAILED
        );
    }

    private function isFailed($result): bool
    {
        return
            false === $result
            || (is_array($result) && isset($result['result']) && false === $result['result']);
    }

    private function processFailedLog(LeadEventLog $log, PendingEvent $pendingEvent): void
    {
        $this->logger->debug(
            'CAMPAIGN: '.ucfirst($log->getEvent()->getEventType() ?? 'unknown event').' ID# '.$log->getEvent()->getId().' for contact ID# '.$log->getLead()->getId()
        );

        $metadata = $log->getMetadata();

        $reason = null;
        if (isset($metadata['errors'])) {
            $reason = (is_array($metadata['errors'])) ? implode('<br />', $metadata['errors']) : $metadata['errors'];
        } elseif (isset($metadata['reason'])) {
            $reason = $metadata['reason'];
        }

        $pendingEvent->fail($log, $reason);
    }
}
