<?php

namespace AutoAdmin\Bundle\DependencyInjection;

use AutoAdmin\Bundle\Admin\Controller\Admin\AdminControllerInterface;
use AutoAdmin\Bundle\Admin\DependencyInjection\Configuration\ConfigurationDefault;
use AutoAdmin\Bundle\Admin\DynamicEntity\Administrator;
use AutoAdmin\Bundle\Admin\Model\AdministratorInterface;
use AutoAdmin\Bundle\Content\Breadcrumbs\Generator\BreadcrumbsGeneratorInterface;
use AutoAdmin\Bundle\Content\Configuration\ConfigBuilder as ContentConfigBuilder;
use AutoAdmin\Bundle\Content\Controller\AbstractContentController;
use AutoAdmin\Bundle\Content\Doctrine\StringFunctions\FindInSet;
use AutoAdmin\Bundle\Content\Doctrine\StringFunctions\GroupConcat;
use AutoAdmin\Bundle\Content\Doctrine\StringFunctions\Replace;
use AutoAdmin\Bundle\Content\Logger\LoggerInterface;
use AutoAdmin\Bundle\Content\Model\PageInterface;
use AutoAdmin\Bundle\Content\Model\SlugInterface;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Content\Repository\AbstractContentRepository;
use AutoAdmin\Bundle\Content\Repository\AbstractTranslatableContentRepository;
use AutoAdmin\Bundle\Content\Routing\ContentAutoRouterInterface;
use AutoAdmin\Bundle\DependencyInjection\Configuration\Configuration;
use AutoAdmin\Bundle\DependencyInjection\Configuration\RawConfigurationFixer;
use ReflectionClass;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException;
use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

class AutoAdminExtension extends Extension implements ConfigurationExtensionInterface, PrependExtensionInterface
{
    const DOCTRINE_INTERFACES = [
        PageInterface::class => 'App\\Entity\\Page',
        AdministratorInterface::class => Administrator::class,
    ];

    const CONTAINER_PARAM_DOCTRINE_ALIASES = 'aa_doctrine_entity_aliases';

    public function load(array $configs, ContainerBuilder $container): void
    {
        // prepare and validate config files (mainly class/slugs)
        $configs = (new RawConfigurationFixer())->fixRawConfigs($configs);

        $configs = $this->mergeDefaultActions($configs);

        $configs = $this->addSlugFieldToEntities($configs);
    
        $config = $this->processConfiguration((new Configuration()), $configs);

        $processedConfig = (new ContentConfigBuilder())->build($config);

        $processedConfig = $this->processConfiguration((new Configuration()), [$processedConfig]);

        $container->setParameter('aa_cache.config', $processedConfig['cache'] ?? []);
        $container->setParameter('aa_content.config', $processedConfig['content'] ?? []);
        $container->setParameter('aa_admin_file_config', $processedConfig['admin'] ?? []);
        $container->setParameter('aa_media_config', $processedConfig['media'] ?? []);

        $container->prependExtensionConfig('framework', [
            'annotations' => true,
        ]);

        $this->loadBrickBuilder($config, $container);
        $this->loadCore($config, $container);
        $this->loadMedia($config, $container);
        $this->loadSeo($config, $container);
        $this->loadContent($config, $container);
        $this->loadAdmin($config, $container);
        $this->loadMenu($config, $container);
        $this->loadCache($config, $container);
    }

    protected function loadAdmin(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Admin/Resources/config'));
        $loader->load('services.yaml');
        $loader->load('controllers.yaml');
        $loader->load('config.yaml');
        $loader->load('form.yaml');
        $loader->load('twig.yaml');

        $container->registerForAutoconfiguration(AdminControllerInterface::class)
            ->addTag('aa_admin.controller', ['priority' => -100]);

        $container->registerForAutoconfiguration(AbstractContentRepository::class)
            ->addTag('aa_content.repository', ['priority' => 100]);
        $container->registerForAutoconfiguration(AbstractTranslatableContentRepository::class)
            ->addTag('aa_content.repository', ['priority' => 100]);
    }

    protected function loadBrickBuilder(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../BrickBuilder/Resources/config'));
        $loader->load('services.yaml');
        $loader->load('bricks.yaml');
    }

    protected function loadContent(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Content/Resources/config'));
        $loader->load('services.yaml');
        $loader->load('translation.yaml');
        $container->registerForAutoconfiguration(ContentAutoRouterInterface::class)
            ->addTag('aa_content.auto_router', ['priority' => 0]);

        // tag services extending this class
        $container->registerForAutoconfiguration(AbstractContentController::class)
            ->addTag('content.controller', ['priority' => 100]);

        $container->registerForAutoconfiguration(LoggerInterface::class)
            ->addTag('aa_content.logger', ['priority' => 0]);

        $container->registerForAutoconfiguration(BreadcrumbsGeneratorInterface::class)
            ->addTag('aa_content.breadcrumbs_generator', ['priority' => 0]);
    }

    protected function loadCore(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Core/Resources/config'));
        $loader->load('services.yaml');

        // set defaults for languages
        $parameterBag = $container->getParameterBag();

        if ($parameterBag->has('app_locales')) {
            $appLocales = $parameterBag->get('app_locales');
            $appLocalesArray = explode('|', $appLocales);
        } else {
            $appLocales = 'fr';
            $appLocalesArray = ['fr'];
        }

        // set defaults for languages
        $defaults = [
            'locales' => $appLocalesArray,
            'locales_pattern' => sprintf('(%s)', $appLocales),
            'default_locale' => $appLocalesArray[0]
        ];

        foreach ($defaults as $name => $value) {
            try {
                $container->getParameter($name);
            } catch (ParameterNotFoundException $exception) {
                $container->setParameter($name, $value);
            }
        }
    }

    protected function loadMedia(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Media/Resources/config'));
        $loader->load('services.yaml');
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Media/Resources/config'));
        $loader->load('services.yaml');
    }

    protected function loadMenu(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Menu/Resources/config'));
        $loader->load('services.yaml');
    }

    protected function loadSeo(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Seo/Resources/config'));
        $loader->load('services.yaml');
    }

    protected function loadCache(array $configs, ContainerBuilder $container): void
    {
        $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Cache/Resources/config'));
        $loader->load('services.yaml');
    }

    public function getConfiguration(array $config, ContainerBuilder $container): Configuration
    {
        return new Configuration();
    }

    public function prepend(ContainerBuilder $container): void
    {
        $container->prependExtensionConfig('framework', [
            'translator' => [
                'paths' => [
                    __DIR__.'/../../src/Admin/Resources/translations',
                    __DIR__.'/../../src/BrickBuilder/Resources/translations',
                    __DIR__.'/../../src/Content/Resources/translations',
                    __DIR__.'/../../src/Seo/Resources/translations',
                    __DIR__.'/../../src/Menu/Resources/translations',
                    __DIR__.'/../../src/Media/Resources/translations',
                ],
            ],
        ]);

        $this->prependDoctrineConfig($container);
        $this->prependTwigPaths($container);
        $this->prependKnpMenu($container);
    }

    private function prependDoctrineConfig(ContainerBuilder $container): void
    {
        $mappings = [];

        // resolve_target_entities
        $configs = $container->getExtensionConfig('doctrine');
        $configValues = [];
        foreach (array_reverse($configs) as $config) {
            if (isset($config['orm']['resolve_target_entities'])) {
                $configValues[] = $config['orm']['resolve_target_entities'];
            }
        }
        $flatValues = [];
        foreach ($configValues as $configValue) {
            $flatValues = [...$flatValues, ...$configValue];
        }
        $resolveTargetEntitiesConfig = [
            'resolve_target_entities' => $resolved = [
                ...self::DOCTRINE_INTERFACES,
                ...$flatValues,
            ],
        ];
        $container->setParameter(self::CONTAINER_PARAM_DOCTRINE_ALIASES, $resolved);
        // --resolve_target_entities

        // doctrine entity repos & entity mappings
        foreach (['Admin', 'Content', 'BrickBuilder', 'Seo', 'Menu', 'Cache', 'Media'] as $name) {
            $mappings[$alias = 'AutoAdminBundle'.$name] = [
                'is_bundle' => false,
                'type' => 'attribute',
                'dir' => __DIR__.'/../../src/'.$name.'/Entity',
                'prefix' => 'AutoAdmin\Bundle\\'.$name.'\\Entity',
                'alias' => $alias,
            ];
        }
        // only add special mapping if alias is not overridden
        if ($resolved[AdministratorInterface::class] === self::DOCTRINE_INTERFACES[AdministratorInterface::class]) {
            $name = 'Admin';
            $mappings[$alias = 'AutoAdminBundle'.$name.'Dynamic'] = [
                'is_bundle' => false,
                'type' => 'attribute',
                'dir' => __DIR__.'/../../src/'.$name.'/DynamicEntity',
                'prefix' => 'AutoAdmin\Bundle\\'.$name.'\\DynamicEntity',
                'alias' => $alias,
            ];
        }
        // --doctrine entity repos & entity mappings

        $prependConfig = [
            'orm' => [
                'mappings' => $mappings,
                'dql' => [
                    'string_functions' => [
                        'REPLACE' => Replace::class,
                        'GROUP_CONCAT' => GroupConcat::class,
                        'FIND_IN_SET' => FindInSet::class,
                    ],
                ],
                ...$resolveTargetEntitiesConfig,
            ],
        ];

        /*
         * https://symfony.com/doc/current/doctrine/resolve_target_entity.html
         * https://www.doctrine-project.org/projects/doctrine-orm/en/2.17/cookbook/resolve-target-entity-listener.html
         * https://symfony.com/doc/current/bundles/prepend_extension.html
         *
         * ⚠️️If there is more than one bundle that prepends the same extension and defines the same key,
         * ⚠️the bundle that is registered first will take priority: next bundles won't override this specific config setting.
         */
        $container->prependExtensionConfig('doctrine', $prependConfig);
    }

    private function prependTwigPaths(ContainerBuilder $container): void
    {
        // twig directory paths
        $container->prependExtensionConfig(
            'twig',
            [
                'paths' => [
                    __DIR__.'/../../src/Content/Resources/views' => 'AutoAdminContent',
                    __DIR__.'/../../src/Admin/Resources/views' => 'AutoAdminAdmin',
                    __DIR__.'/../../src/BrickBuilder/Resources/views' => 'AutoAdminBrickBuilder',
                    __DIR__.'/../../src/Menu/Resources/views' => 'AutoAdminMenu',
                    __DIR__.'/../../src/Media/Resources/views' => 'AutoAdminMedia',
                ],
            ]
        );
    }

    private function prependKnpMenu(ContainerBuilder $container): void
    {
        // menu default template
        $bundles = $container->getParameter('kernel.bundles');
        if (in_array('Knp\\Bundle\\MenuBundle\\KnpMenuBundle', $bundles, true)) {
            $config = [
                'twig' => [
                    // 'template' => 'KnpMenuBundle::menu.html.twig',
                    'template' => '@AutoAdminMenu/menu.html.twig',
                ],
            ];
            $container->prependExtensionConfig('knp_menu', $config);
        }
    }

    private function mergeDefaultActions(array $configs): array
    {
        foreach ($configs as $keyConfig => $config) {
            if (!isset($config['admin']['entities'])) {
                continue;
            }

            foreach ($config['admin']['entities'] as $classname => $entity) {
                if (!isset($entity['actions'])) {
                    continue;
                }

                foreach ($entity['actions'] as $keyAction => $action) {
                    $defaultAction = array_filter(
                        ConfigurationDefault::getDefaultActions(),
                        function ($defaultAction) use ($action) {
                            return $defaultAction['action'] === $action['action'];
                        }
                    );
                    $defaultAction = array_shift($defaultAction);
                    if (!$defaultAction) {
                        continue;
                    }

                    $configs[$keyConfig]['admin']['entities'][$classname]['actions'][$keyAction] = array_merge(
                        $defaultAction,
                        $action
                    );
                }
            }
        }

        return $configs;
    }

    private function addSlugFieldToEntities(array $configs): array
    {
        foreach ($configs as $keyConfig => $config) {
            if (!isset($config['admin']['entities'])) {
                continue;
            }
    
            foreach ($config['admin']['entities'] as $classname => $entity) {
                if (!$this->classImplementsSlugInterface($classname)) {
                    continue;
                }

                if (!isset($entity['form']['update'])) {
                    continue;
                }

                $slugProperty = $this->determineSlugProperty($classname);

                $slugFieldExists = false;
                foreach ($entity['form']['update'] as $field) {
                    if (isset($field['property']) && $field['property'] === $slugProperty) {
                        $slugFieldExists = true;
                        break;
                    }
                }

                if (!$slugFieldExists) {
                    $configs[$keyConfig]['admin']['entities'][$classname]['form']['update'][] = [
                        'property' => $slugProperty,
                        'label' => 'Slug',
                        'roles' => ['ROLE_SUPER_ADMIN'],
                    ];
                }
            }
        }
        
        return $configs;
    }

    private function classImplementsSlugInterface(string $className): bool
    {
        if (!class_exists($className)) {
            return false;
        }

        $reflection = new ReflectionClass($className);
        if ($reflection->implementsInterface(SlugInterface::class)) {
            return true;
        }

        if ($reflection->implementsInterface(TranslatableInterface::class)) {
            return $this->classImplementsSlugInterface($className . 'Translation');
        }
        
        return false;
    }

    private function determineSlugProperty(string $className): string
    {
        $reflection = new ReflectionClass($className);

        return $reflection->implementsInterface(TranslatableInterface::class)
            ? '_tr.slug'
            : 'slug';
    }
}
