<?php

namespace KupShop\KupShopBundle\Util\Event;

use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Exception\CronException;
use KupShop\KupShopBundle\Util\Contexts;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\Event;

class LoggingEventDispatcher implements EventDispatcherInterface
{
    private $eventDispatcher;

    private $logger;

    private $specificMethod;

    public function __construct(EventDispatcherInterface $eventDispatcher, LoggerInterface $logger,
        protected readonly ContextManager $contextManager)
    {
        $this->eventDispatcher = $eventDispatcher;
        $this->logger = $logger;
    }

    public function dispatch(object $event, ?string $eventName = null): object
    {
        if (null === $event) {
            $event = new Event();
        }

        $eventName = $eventName ?? \get_class($event);

        if ($listeners = $this->getListeners($eventName)) {
            $this->doDispatch($listeners, $eventName, $event);
        }

        return $event;
    }

    public function removeSubscriber(EventSubscriberInterface $subscriber): void
    {
        $this->eventDispatcher->removeSubscriber($subscriber);
    }

    public function removeListener($eventName, $listener): void
    {
        $this->eventDispatcher->removeListener($eventName, $listener);
    }

    public function addListener($eventName, $listener, $priority = 0): void
    {
        $this->eventDispatcher->addListener($eventName, $listener, $priority);
    }

    public function hasListeners($eventName = null): bool
    {
        return $this->eventDispatcher->hasListeners($eventName);
    }

    public function addSubscriber(EventSubscriberInterface $subscriber): void
    {
        $this->eventDispatcher->addSubscriber($subscriber);
    }

    public function getListenerPriority($eventName, $listener): ?int
    {
        return $this->eventDispatcher->getListenerPriority($eventName, $listener);
    }

    public function getListeners($eventName = null): array
    {
        return $this->eventDispatcher->getListeners($eventName);
    }

    protected function doDispatch($listeners, $eventName, Event $event): void
    {
        foreach ($listeners as $listener) {
            if ($event->isPropagationStopped()) {
                break;
            }

            if ($this->specificMethod
                && $listener[1] != $this->specificMethod
                && sprintf('%s::%s', get_class($listener[0]), $listener[1]) != $this->specificMethod
            ) {
                continue;
            }

            $timeStart = microtime(true);

            // Log dispatch of event
            $this->logger->notice('Dispatching event: '.$eventName, [
                'eventName' => $eventName,
                'listener' => $this->getListenerName($listener),
            ]);

            try {
                // Dispatch event
                \call_user_func($listener, $event, $eventName, $this);
            } catch (\Throwable $e) {
                if (isDevelopment()) {
                    throw $e;
                }

                $sentry = getRaven();
                $sentry->captureException(
                    new CronException($this->getListenerName($listener).': '.$e->getMessage(), intval($e->getCode()), $e)
                );
            } finally {
                // Make sure contexts change did not leak outside event handler
                if (Contexts::get(LanguageContext::class)->getActiveId() != Contexts::get(LanguageContext::class)->getDefaultId()) {
                    Contexts::get(LanguageContext::class)->activate(Contexts::get(LanguageContext::class)->getDefaultId());

                    // TMP: Report to Sentry
                    $sentry = getRaven();
                    $sentry->captureException(
                        new CronException($this->getListenerName($listener).': Language context leak!')
                    );
                }
                if ($leaks = $this->contextManager->checkEmptyContextsLeak()) {
                    // TMP: Report to Sentry
                    $sentry = getRaven();
                    $sentry->captureException(
                        new CronException($this->getListenerName($listener).': Emptiable context leak!'),
                        ['leaks' => array_keys($leaks)]
                    );

                    $this->contextManager->forceEmptyContexts();
                }
            }

            // Log event was finished
            $this->logger->notice('Finished event dispatch: '.$eventName, [
                'eventName' => $eventName,
                'listener' => $this->getListenerName($listener),
                'execTime' => microtime(true) - $timeStart,
            ]);
        }
    }

    private function getListenerName($listener): string
    {
        $name = \is_object($listener[0]) ? \get_class($listener[0]) : $listener[0];

        return $name.'::'.$listener[1];
    }

    public function setSpecificMethod($specificMethod): void
    {
        $this->specificMethod = $specificMethod;
    }
}
