<?php

namespace AutoAdmin\Bundle\BrickBuilder\Doctrine\EventListener;

use AutoAdmin\Bundle\BrickBuilder\Brick\BrickConfigManager;
use AutoAdmin\Bundle\BrickBuilder\Brick\Config\BrickConfiguration;
use AutoAdmin\Bundle\BrickBuilder\Brick\ConfigSetting\EntityCollectionSetting;
use AutoAdmin\Bundle\BrickBuilder\Brick\ConfigSetting\EntitySetting;
use AutoAdmin\Bundle\BrickBuilder\Brick\ConfigSetting\ScalarCollectionSetting;
use AutoAdmin\Bundle\BrickBuilder\Brick\ConfigSetting\ScalarSetting;
use AutoAdmin\Bundle\BrickBuilder\Brick\ConfigSetting\SubEntityCollectionSetting;
use AutoAdmin\Bundle\BrickBuilder\Exception\BadSettingFormatException;
use AutoAdmin\Bundle\BrickBuilder\Model\AbstractBrickSubContent;
use AutoAdmin\Bundle\BrickBuilder\Model\BrickInterface;
use AutoAdmin\Bundle\BrickBuilder\Storage\BrickStorage;
use AutoAdmin\Bundle\BrickBuilder\Storage\ObjectSettingFormat;
use AutoAdmin\Bundle\Core\Doctrine\ListenerStateManager;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Exception;
use ReflectionClass;

class BrickListener implements EventSubscriber
{
    private array $trackedBricks = [];
    private array $pending = [];
    private array $toUpdate = [];
    private array $toRemove = [];

    public function __construct(
        protected BrickConfigManager $configManager,
        protected EntityManagerInterface $em,
        protected ObjectSettingFormat $objectSettingFormat,
        protected ListenerStateManager $listenerStateManager,
        protected BrickStorage $brickStorage,

    ) {
    }

    public function getSubscribedEvents(): array
    {
        return [
            // load
            Events::postLoad,
            // update
            Events::preUpdate,
            Events::postUpdate,
            // persist
            Events::prePersist,
            Events::postPersist,
        ];
    }

    /**
     * @throws Exception
     */
    public function postLoad(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();
        if ($entity instanceof BrickInterface) {
            $this->brickStorage->fromDatabaseFormat($entity);
        }
    }

    /**
     * @throws Exception
     */
    public function preUpdate(LifecycleEventArgs $args): void
    {
        $this->store($args);
    }

    public function postUpdate(LifecycleEventArgs $args): void
    {
        $this->removeObjects($args);
        $this->updateObjects($args);
    }

    /**
     * @throws Exception
     */
    public function prePersist(LifecycleEventArgs $args): void
    {
        $this->store($args);
    }

    public function postPersist(LifecycleEventArgs $args): void
    {

        $this->updatePendingObjects($args);
        $this->clearPendingObjects($args);

        $this->removeObjects($args);
        $this->updateObjects($args);
    }

    protected function updatePendingObjects(LifecycleEventArgs $args): void
    {
        $object = $args->getObject();
        if ($object instanceof BrickInterface && $this->pending) {

            // saved referenced objects
            foreach ($this->pending as $splHash => $data) {
                foreach ($data as $key => $settingConf) {
                    $entities = $settingConf['entities'] ?? [];
                    foreach ($entities as $entity) {
                        $this->em->persist($entity);
                    }
                }

            }
            $this->em->flush();

            // refresh objects
            foreach ($this->pending as $splHash => $data) {
                foreach ($data as $key => $settingConf) {
                    $entities = $settingConf['entities'] ?? [];
                    foreach ($entities as $entity) {
                        $this->em->refresh($entity);
                    }
                }
            }

            // update bricks with saved objects
            foreach ($this->pending as $splHash => $data) {
                foreach ($data as $key => $settingConf) {
                    /** @var BrickInterface $brick */
                    $brick = $settingConf['brick'];
                    $entities = $settingConf['entities'] ?? [];

                    /** @var BrickConfiguration $configuration */
                    $configuration = $this->configManager->getHandlerConfiguration($brick->getType());
                    $settingsConfig = $configuration->getSettings();

                    $val = match ($settingsConfig[$key]->getType()) {
                        EntitySetting::class => $entities[0] ?? null,
                        EntityCollectionSetting::class, SubEntityCollectionSetting::class => $entities,
                        default => throw new Exception('Unexpected setting type in pending objects handling'),
                    };

                    $brick->setSettings([
                        ...$brick->getSettings(),
                        ...[$key => $val,],
                    ]);
                    $this->brickStorage->toDatabaseFormat($brick);
                    $this->em->persist($brick);

                    $this->pending[$splHash][$key]['processed'] = true;
                }
            }

            $this->em->flush();
        }
    }

    protected function clearPendingObjects(LifecycleEventArgs $args): void
    {
        $object = $args->getObject();
        if ($object instanceof BrickInterface && $this->pending) {
            foreach ($this->pending as $splHash => $data) {
                foreach ($data as $key => $settingConf) {
                    if (($this->pending[$splHash][$key]['processed'] ?? null) === true) {
                        unset($this->pending[$splHash][$key]);
                        if (empty($this->pending[$splHash][$key])) {
                            unset($this->pending[$splHash]);
                        }
                    }
                }
            }
        }
    }

    protected function updateObjects(LifecycleEventArgs $args): void
    {
        $object = $args->getObject();
        if ($object instanceof BrickInterface && $this->toUpdate) {
            foreach ($this->toUpdate as $key => $item) {
                $this->em->persist($item);
                unset($this->toUpdate[$key]);
            }
            $this->em->flush();
        }
    }

    protected function removeObjects(LifecycleEventArgs $args): void
    {
        $object = $args->getObject();
        if ($object instanceof BrickInterface && $this->toRemove) {
            foreach ($this->toRemove as $key => $item) {
                $this->em->remove($item);
                unset($this->toRemove[$key]);
            }
            $this->em->flush();
        }
    }

    /**
     * @throws Exception
     */
    protected function store(LifecycleEventArgs $args): void
    {
        $entity = $args->getObject();

        if ($entity instanceof BrickInterface) {
            $splHash = spl_object_hash($entity);
            // only handle brick storage once per flush, avoid recursion
            if (in_array($splHash, $this->trackedBricks)) {
                return;
            }
            $this->trackedBricks[] = $splHash;

            try {
                $this->brickStorage->toDatabaseFormat($entity);
                $this->checkRemovedItems($entity);
                $this->checkPendingItems($entity);

                $this->removeObjects($args);
                $this->updateObjects($args);
                $this->brickStorage->toDatabaseFormat($entity);// rerun after checks, settings array may be altered
            } catch (Exception $exception) {
                throw $exception;
            }
        }
    }

    /**
     * @throws Exception
     */
    protected function checkRemovedItems(BrickInterface $brick): void
    {
        $changeSet = $this->em->getUnitOfWork()->getEntityChangeSet($brick);
        if (empty($changeSet['dbSettings'])) {
            return;
        }
        $settingsChangeSet = $changeSet['dbSettings'];
        [$before, $after] = $settingsChangeSet;
        /** @var BrickConfiguration $configuration */
        $configuration = $this->configManager->getHandlerConfiguration($brick->getType());
        $settingsConfig = $configuration->getSettings();
        $settings = $brick->getSettings();
        foreach ($settings as $key => $val) {
            if (!array_key_exists($key, $configuration->getSettings())) {
                continue;
            }
            $settingConfig = $settingsConfig[$key];
            switch ($currentType = $settingConfig->getType()) {
                case EntitySetting::class:
                    $old = $before[$key] ?? null;
//                    $new = $this->objectSettingFormat->objectToString($after[$key]);
                    $new = $after[$key];

                    // if we may have a divergence, try to get source object
                    if (!empty($old) && $new !== $old) {
                        if (is_string($old)) {
                            $old = $this->objectSettingFormat->stringToObject($old);
                            if (null !== $old) {
                                $this->removeIfNeeded($old, $new);
                            }
                        }
                    }
                    break;
                case EntityCollectionSetting::class:
                case SubEntityCollectionSetting::class:
                    $val = array_filter($val);
                    $oldValues = [];
                    if (!empty($before[$key])) {
                        foreach ($before[$key] as $item) {
                            if (!is_string($item)) {
                                throw new BadSettingFormatException($brick, $key, 'in old content building');
                            }
                            $oldValues[] = $this->objectSettingFormat->stringToObject($item);
                        }
                    }

                    $newValues = [];
                    foreach ($after[$key] as $item) {
                        $newValues[] = $item;
                    }

                    foreach ($oldValues as $item) {
                        $found = false;
                        foreach ($newValues as $newValue) {
                            if ($newValue === $item) {
                                $found = true;
                            }
                        }

                        if ($found) {
                            continue;
                        }

                        // only cascade delete objects for SubEntityCollectionSetting
                        if ($currentType === SubEntityCollectionSetting::class && $item) {
                            foreach ($val as $k => $v) {
                                if ($item === $v) {
                                    unset($val[$k]);
                                }
                            }
                            $this->toRemove[] = $item;
                        }
                    }
                    $settings[$key] = $val;
                    break;
                case ScalarSetting::class:
                case ScalarCollectionSetting::class:
                default:
                    break;
            }
        }
        $brick->setSettings($settings);
    }

    protected function checkPendingItems(BrickInterface $brick): void
    {
        /** @var BrickConfiguration $configuration */
        $configuration = $this->configManager->getHandlerConfiguration($brick->getType());
        $settingsConfig = $configuration->getSettings();
        $settings = $brick->getSettings();
        $splHash = spl_object_hash($brick);
        foreach ($settings as $key => $val) {
            if (!array_key_exists($key, $configuration->getSettings())) {
                continue;
            }
            $settingConfig = $settingsConfig[$key];
            switch ($currentType = $settingConfig->getType()) {
                case EntitySetting::class:
                    if (!empty($val) && is_object($val)) {
                        $this->pending[$splHash] = [
                            ...$this->pending[$splHash] ?? [],
                            ...[
                                $key => [
                                    'brick' => $brick,
                                    'processed' => false,
                                    'entities' => [$val],
                                ],
                            ],
                        ];
                    }
                    break;
                case EntityCollectionSetting::class:
                case SubEntityCollectionSetting::class:
                    $val = array_filter($val);
                    $entities = [];
                    foreach ($val as $item) {
                        $entities[] = $item;
                    }
                    if ($entities) {
                        $this->pending[$splHash] = [
                            ...$this->pending[$splHash] ?? [],
                            ...[
                                $key => [
                                    'brick' => $brick,
                                    'processed' => false,
                                    'entities' => $entities,
                                ],
                            ],
                        ];
                    }
                    break;
                case ScalarSetting::class:
                case ScalarCollectionSetting::class:
                default:
                    break;
            }
        }
    }


    /**
     * @throws Exception
     */
    protected function toDatabaseValue($object): ?string
    {
        if (is_string($object)) {
            $object = $this->objectSettingFormat->stringToObject($object);
        }

        if (is_null($object)) {
            return null;
        }

        $em = $this->em;
        if (empty($object->getId())) {
//            throw new \RuntimeException('Brick setting value of type "'.get_class($object).'" is not persisted.');

            // save un-persisted entity as we need its id to transform
            $em->persist($object);
            $em->flush();
            $em->refresh($object);
        } else {
            $this->toUpdate[] = $object;
        }

        return $this->objectSettingFormat->objectToString($object);
    }

    /**
     * @throws Exception
     */
    private function removeIfNeeded(object $old, ?string $new): void
    {
        $refl = new ReflectionClass($old);
        if (!$refl->isSubclassOf(AbstractBrickSubContent::class)) {
            return;
        }

        // 2 cases:
        if ($new === null) {
            // - $old is an object with a given id and $new is null, always delete
            $this->toRemove[] = $old;
        } else {
            $new = $this->objectSettingFormat->stringToObject($new);
            // - $old is an object with a given id and $new is an object with a different id, delete old only if id is different
            if ($old->getId() != $new->getId()) {
                $this->toRemove[] = $old;
            }
        }
    }
}
