<?php

namespace AutoAdmin\Bundle\Content\EventListener;

use AutoAdmin\Bundle\Admin\Entity\Administrator;
use AutoAdmin\Bundle\Admin\Entity\CookieConsent;
use AutoAdmin\Bundle\Admin\Router\AdminRouter;
use AutoAdmin\Bundle\BrickBuilder\Brick\Handler\RootBrickHandler;
use AutoAdmin\Bundle\BrickBuilder\Model\BrickInterface;
use AutoAdmin\Bundle\Cache\Entity\CacheLog;
use AutoAdmin\Bundle\Content\Entity\AbstractTranslation;
use AutoAdmin\Bundle\Content\Entity\Log;
use AutoAdmin\Bundle\Content\Model\NameInterface;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Content\Model\TranslationInterface;
use AutoAdmin\Bundle\Content\Routing\ContentRouter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use ReflectionClass;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class LoggingListener
{
    private TokenStorageInterface $tokenStorage;
    private ContentRouter $contentRouter;
    private AdminRouter $adminRouter;
    private EntityManagerInterface $entityManager;
    private ManagerRegistry $managerRegistry;
    private Request $request;

    private array $brickUpdated = [];
    private string $appEnv;

    public function __construct(
        TokenStorageInterface $tokenStorage,
        ContentRouter $contentRouter,
        AdminRouter $adminRouter,
        ManagerRegistry $managerRegistry,
        RequestStack $requestStack,
        string $appEnv
    ) {
        $this->tokenStorage = $tokenStorage;
        $this->contentRouter = $contentRouter;
        $this->adminRouter = $adminRouter;
        $this->entityManager = $managerRegistry->getManager();
        $this->managerRegistry = $managerRegistry;
        $this->request = $requestStack->getCurrentRequest();
        $this->appEnv = $appEnv;
    }

    public function preUpdate(PreUpdateEventArgs $event)
    {
        if (isset($event->getEntityChangeSet()['settings'])) {
            $this->brickUpdated[] = $event->getObject()->getId();
        }
    }

    public function onFlush()
    {
        $uow = $this->entityManager->getUnitOfWork();

        foreach ($uow->getScheduledEntityUpdates() as $entity) {
            $changes = $uow->getEntityChangeSet($entity);
            if (empty($changes)) {
                continue;
            }

            $this->handleLog($entity, Log::TYPE_UPDATE);
        }

        foreach ($uow->getScheduledEntityDeletions() as $entity) {
            $this->handleLog($entity, Log::TYPE_DELETE);
        }

        foreach ($uow->getScheduledEntityInsertions() as $entity) {
            $this->handleLog($entity, Log::TYPE_CREATE);
        }
    }

    private function handleLog(object $entity, string $type)
    {
        if (!$this->isAllowed($entity)) {
            return;
        }

        if ($entity instanceof BrickInterface) {
            $this->handleBrickLog($entity, $type);
        } else {
            $this->createLog($entity, $type);
        }
    }

    private function isAllowed(object $entity): bool
    {
        return !$entity instanceof Log
            && !$entity instanceof AbstractTranslation
            && !$entity instanceof CacheLog
            && !$entity instanceof CookieConsent
            && $this->appEnv !== 'test';
    }

    private function createLog(object $entity, string $type)
    {
        $reflect = new ReflectionClass($entity);

        $log = new Log();
        if (
            $this->tokenStorage->getToken()
            && ($user = $this->tokenStorage->getToken()->getUser())
            && $user instanceof Administrator
        ) {
            $log->setAdministrator($user);
        }
        $log->setIp($this->request->getClientIp());
        $log->setType($type);
        $log->setEntity($reflect->getShortName());

        if ($reflect->implementsInterface(TranslatableInterface::class) && $entity->getCurrentTranslation()) {
            if ((new ReflectionClass($entity->getCurrentTranslation()))->implementsInterface(NameInterface::class)) {
                $log->setTitle($entity->getCurrentTranslation()->getName());
            }
        }

        try {
            $log->setFrontLink($this->contentRouter->generate($entity));
        } catch (Exception $e) {
            $log->setFrontLink(null);
        }

        try {
            if ($type !== Log::TYPE_DELETE) {
                $log->setAdminLink($this->adminRouter->generate($entity, 'update'));
            }
        } catch (Exception $e) {
            $log->setAdminLink(null);
        }

        $this->entityManager->persist($log);

        $classMetadata = $this->entityManager->getClassMetadata(Log::class);
        $uow = $this->entityManager->getUnitOfWork();
        $uow->computeChangeSet($classMetadata, $log);
    }

    private function handleBrickLog(BrickInterface $brick, string $type)
    {
        if ($type === Log::TYPE_UPDATE && !in_array($brick->getId(), $this->brickUpdated)) {
            return;
        }

        $rootBrick = null;
        while ($rootBrick === null) {
            if ($brick->getType() === RootBrickHandler::BRICK_TYPE) {
                $rootBrick = $brick;
                break;
            } elseif ($brick->getParent() === null) {
                break;
            }
            $brick = $brick->getParent();
        }

        foreach (get_declared_classes() as $class) {
            $reflect = new ReflectionClass($class);
            if (!$reflect->implementsInterface(\AutoAdmin\Bundle\Content\Model\BrickInterface::class)) {
                continue;
            }

            $entity = $this->managerRegistry->getRepository($class)->findOneBy([
                'brick' => $rootBrick,
            ]);

            if (!$entity) {
                continue;
            }

            if ($entity instanceof TranslationInterface) {
                $entity = $entity->getTranslatable();
            }

            if ($this->mustBeCreated($entity, $type.'_brick')) {
                $this->createLog($entity, $type.'_brick');
            }
        }
    }

    private function mustBeCreated(object $entity, string $type): bool
    {
        $logs = $this->managerRegistry->getRepository(Log::class)->findLessThan(
            (new ReflectionClass($entity))->getShortName(),
            $type
        );

        return empty($logs);
    }
}
