<?php

namespace AutoAdmin\Bundle\Admin\Configuration;

use AutoAdmin\Bundle\Admin\Attribute\Loader\AttributeLoader;
use AutoAdmin\Bundle\Admin\Attribute\PropertyAttributes;
use AutoAdmin\Bundle\Admin\Controller\Admin\EntityController;
use AutoAdmin\Bundle\Admin\DependencyInjection\Configuration\Configuration;
use AutoAdmin\Bundle\Admin\DependencyInjection\Configuration\ConfigurationDefault;
use AutoAdmin\Bundle\Admin\Exception\UnknownActionException;
use AutoAdmin\Bundle\Admin\Form\Type\Component\RichText;
use AutoAdmin\Bundle\Admin\Form\TypeGuesser\TypeGuesserManager;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Core\Slugger\Slugger;
use AutoAdmin\Bundle\Core\Util\CaseConverter;
use AutoAdmin\Bundle\Seo\Form\SeoMetadataType;
use AutoAdmin\Bundle\Seo\Model\SeoContentInterface;
use AutoAdmin\Bundle\Seo\Model\SeoContentTranslationInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Translation\Translator;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Class ConfigManager
 *
 * @package AutoAdmin\Bundle\Admin\Configuration
 *
 * Inspired by https://github.com/EasyCorp/EasyAdminBundle/blob/2.x/src/Configuration/ConfigManager.php
 */
final class ConfigBuilder
{
    private Slugger $slugger;
    private array $extraConfigProcessors;
    private CaseConverter $caseConverter;
    private TypeGuesserManager $typeGuesserManager;
    private AttributeLoader $attributeLoader;
    private TranslatorInterface $translator;


    /**
     * ConfigManager constructor.
     *
     * @param ExtraConfigInterface[] $extraConfigProcessors
     */
    public function __construct(
        Slugger $slugger,
        CaseConverter $caseConverter,
        AttributeLoader $attributeLoader,
        TypeGuesserManager $typeGuesserManager,
        iterable $extraConfigProcessors,
        TranslatorInterface $translator
    ) {
        $this->extraConfigProcessors = $extraConfigProcessors;
        $this->slugger = $slugger;
        $this->caseConverter = $caseConverter;
        $this->typeGuesserManager = $typeGuesserManager;
        $this->attributeLoader = $attributeLoader;
        $this->translator = $translator;
    }

    /**
     * @throws ReflectionException
     */
    public function build(array $config): array
    {
        $config = $this->applyExtraConfigurators($config);

        $manipulator = new ConfigManipulator($config);

        // add defaults
        $manipulator->addController(EntityController::class);

        $this->normalizeControllers($manipulator);

        $this->normalizeActions($manipulator);

        // add controllers found in entities actions or in menu items
        $this->addExoticControllers($manipulator);

        // apply role precedence [global < entity < action]
        $this->processEntityRoles($manipulator);

        // default entity modifications & checks
        $this->setupEntities($manipulator);

        // auto-register certain actions
        $this->processEntityActionZones($manipulator);

        // controller actions
        $this->processControllers($manipulator);

        // prepare actions defaults (mainly routes and routeParameters)
        $this->processActionRoutes($manipulator);

        // form fields
        $this->processFormConfig($manipulator);

        // tabs
        $this->createTabs($manipulator);
        $this->appendBuilderTabs($manipulator);// may be external ?
        $this->appendSeoTabs($manipulator);// may be external ?

        // prepare "entity_action" typed actions by copying target informations
        $this->applyLinkedActions($manipulator);

        // prepare admin menu
        $this->processMenu($manipulator);

        $config = $manipulator->getConfig();

        // reapply config, if php broke something before, we should have an error
        $config = $this->processConfig($config);

        return $config;
    }

    /**
     * Re-apply backend configuration
     *
     * @param array $config
     * @return array
     */
    protected function processConfig(array $config): array
    {
        $processor = new Processor();
        $configuration = new Configuration();

        return $processor->processConfiguration($configuration, [$config]);
    }

    /**
     * Apply third party configurations using ExtraConfigInterface
     *
     * @param array $config
     * @return array
     */
    protected function applyExtraConfigurators(array $config)
    {
        $extraConfigHelper = new ExtraConfigHelper($config);

        foreach ($this->extraConfigProcessors as $processor) {
            $processor->process($extraConfigHelper);
        }

        // here, re-apply configuration, this ensures even default values are re-normalized
        $config = $this->processConfig($extraConfigHelper->getConfig());

        return $config;
    }

    protected function processRoleHierarchy(array $parent, array $childRules)
    {
        $processedRoles = $parent;
        if (!empty($childRules)) {
            $modifierList = array_map(
                function ($v) {
                    return substr($v, 0, 1);
                },
                $childRules
            );

            $hasModifiers = in_array('-', $modifierList) || in_array('+', $modifierList);
            if (!$hasModifiers) {
                // role without modifier, we OVERWRITE parent roles
                $processedRoles = $childRules;
            } else {
                // role with modifier, we INHERIT parent roles (even if one of the roles has no modifiers)
                foreach ($childRules as $role) {
                    $modifier = null;
                    if (in_array(substr($role, 0, 1), ['-', '+'])) {
                        $modifier = substr($role, 0, 1);
                        $role = substr($role, 1);
                    }
                    switch ($modifier) {
                        // remove
                        case '-':
                            if (in_array($role, $processedRoles)) {
                                $idx = array_search($role, $processedRoles);
                                unset($processedRoles[$idx]);
                            }
                            break;
                        // default to add
                        default:
                        case '+':
                            if (!in_array($role, $processedRoles)) {
                                $processedRoles[] = $role;
                            }
                            break;
                    }
                }
            }
        }

        return $processedRoles;
    }

    protected function processActionRoutes(ConfigManipulator $manipulator)
    {
        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            foreach ($manipulator->getEntityActions($entityFqcn) as $key => $action) {
                switch ($action['type']) {
                    case 'default':
                        $controller = $entityConfig['controller'];
                        $controllerRefl = new ReflectionClass($controller);
                        $method = $key;
                        if (!$controllerRefl->hasMethod($method)) {
                            $this->error(
                                'No method "%s" in controller "%s" for entity "%s"',
                                [$method, $controller, $entityConfig['class'],]
                            );
                        }
                        $actionParam = [];
                        if (
                            $controller == EntityController::class
                            || $controllerRefl->isSubclassOf(EntityController::class)
                        ) {
                            $actionParam['route'] = 'aa_admin_entity';
                            $actionParam['routeParameters'] = array_merge(
                                [
                                    'entitySlug' => $entityConfig['slug'],
                                    'action' => $key,
                                ],
                                $this->handleSortingParameters($entityConfig, $key, $action)
                            );
                        } else {
                            $actionParam['route'] = 'aa_admin_controller';
                            $actionParam['routeParameters'] = [
                                'controller' => $this->createControllerSlug($controller),
                                'method' => $method,
                            ];
                        }
                        $manipulator->setEntityActionProps($entityFqcn, $key, $actionParam);

                        break;

                    case 'controller':
                        if (empty($action['target_controller'])) {
                            throw new \Exception(
                                sprintf(
                                    'No controller target with class "%s" defined in "custom" action.',
                                    $key
                                )
                            );
                        }

                        $controllerRefl = new ReflectionClass($controller = $action['target_controller']);
                        if (!$controllerRefl->hasMethod($method = $action['target_method'])) {
                            $this->error(
                                'No method "%s" in controller "%s" for entity "%s"',
                                [$method, $controller, $entityConfig['class'],]
                            );
                        }
                        $manipulator->setEntityActionProps(
                            $entityFqcn,
                            $key,
                            [
                                'route' => 'aa_admin_controller',
                                'routeParameters' => [
                                    'controller' => $controller,
                                    'method' => $method,
                                ],
                            ]
                        );
                        break;
                }
            }
        }
    }

    public function createControllerSlug(string $controllerFqcn)
    {
        $reflectionClass = new ReflectionClass($controllerFqcn);

        return str_replace(
            '-controller',
            '',
            $this->caseConverter->camelToKebabCase($reflectionClass->getShortName())
        );
    }

    protected function processFormConfig(ConfigManipulator $manipulator)
    {
        foreach ($manipulator->getEntities() as $entity => $entityConfig) {
            $formConfig = $entityConfig['form'];

            if (!empty($formConfig['both'])) {
                $formConfig['create'] = $formConfig['update'] = $formConfig['both'];
                unset($formConfig['both']);
            }

            $entityClasses = ['class' => $entityConfig['class']];
            if (!empty($entityConfig['translation_class'])) {
                $entityClasses['translation_class'] = $entityConfig['translation_class'];
            }

            foreach ($entityClasses as $entityClass) {
                $refl = new ReflectionClass($entityClass);
                foreach ($formConfig as $action => $tabs) {
                    foreach ($tabs as $tab => $fields) {
                        foreach ($fields as $k => $fieldConfig) {
                            $property = $fieldConfig['property'];
                            $isTranslationProp = str_starts_with($property, '_tr.');
                            $isTranslationClass = str_ends_with($entityClass, 'Translation');
                            $property = str_replace('_tr.', '', $property);
                            if (
                                ($isTranslationProp && !$isTranslationClass)
                                || (!$isTranslationProp && $isTranslationClass)
                            ) {
                                continue;
                            }

                            // check if class has this property
                            if (!$refl->hasProperty($property)) {
                                $this->error('No property "%s" in class "%s"', [$property, $entityClass]);
                            }

                            $guessed = $this->typeGuesserManager->guess($fieldConfig, $entityClass, $property);
                            if ($guessed !== null) {
                                $fieldConfig = array_merge(
                                    $fieldConfig,
                                    [
                                        'type' => $guessed->getType(),
                                        'type_options' => array_merge(
                                            $guessed->getOptions(),
                                            $fieldConfig['type_options'] ?? []
                                        ),
                                    ]
                                );
                            } else {
                                $this->error(
                                    'Could not find matching form type "%s" for "%s::%s"',
                                    [$fieldConfig['type'] ?? null, $entityClass, $property,]
                                );
                            }

                            // set form label using field "label" index
                            if (isset($fieldConfig['label'])) {
                                $fieldConfig['type_options']['label'] = $fieldConfig['label'];
                            }

                            // we want auto configuration if nothing is declared in config files
                            if ($fieldConfig['required'] === null) {
                                unset($fieldConfig['required']);
                            }

                            // if root config "required" is set, report its value to form_config
                            if (array_key_exists('required', $fieldConfig)) {
                                $fieldConfig['type_options']['required'] = $fieldConfig['required'];
                            }

                            // default empty data for text types, could be crossed with entity property
                            $sqlTextTypes = [
                                TextType::class,
                                TextareaType::class,
                                RichText::class,
                            ];
                            if (
                                !array_key_exists('empty_data', $fieldConfig['type_options'])
                                && in_array($fieldConfig['type'], $sqlTextTypes)
                            ) {
                                $fieldConfig['type_options']['empty_data'] = '';
                            }

                            $defaultsIfNotSet = [
                                'required' => ($fieldConfig['type'] !== CheckboxType::class),
                                'translation_domain' => 'aa_admin',
                            ];
                            foreach ($defaultsIfNotSet as $key => $default) {
                                if (!array_key_exists($key, $fieldConfig['type_options'])) {
                                    $fieldConfig['type_options'][$key] = $default;
                                }
                            }

                            $fields[$k] = $fieldConfig;
                        }
                        $tabs[$tab] = $fields;
                    }
                    $formConfig[$action] = $tabs;
                }
            }
            $manipulator->setEntityProp($entity, 'form', $formConfig);
        }
    }

    protected function createTabs(ConfigManipulator $manipulator): void
    {
        foreach ($manipulator->getEntities() as $entiyFqcn => $entityConfig) {
            foreach ($entityConfig['form'] as $action => $tabs) {
                foreach ($tabs as $tab => $fields) {
                    foreach ($fields as $field) {
                        if (!$manipulator->hasTab($entiyFqcn, $action, $field['admin_tab'])) {
                            $manipulator->addTab(
                                $entiyFqcn,
                                $action,
                                $field['admin_tab'],
                                [
                                    'label' => sprintf('tab.%s.label', $tab),
                                    'icon' => 'fa fa-cog',
                                    'routeParams' => [
                                        'tab' => $tab,
                                    ],
                                ]
                            );
                        }
                    }
                }
            }
        }
    }

    protected function appendBuilderTabs(ConfigManipulator $manipulator): void
    {
        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            $translatedClass = $entityConfig['translation_class'] ?? null;

            foreach (array_filter([$entityConfig['class'], $translatedClass]) as $class) {
                $attributes = $this->attributeLoader->load($class);
                $builderAttr = 'AutoAdmin\\Bundle\\BrickBuilder\\Attributes\\BrickBuilder';
                $builderAttributes = $attributes->getAttributesByType($builderAttr);

                /** @var PropertyAttributes $propertyAttributes */
                foreach ($builderAttributes as $property => $propertyAttributes) {
                    $property = $translatedClass && $class === $translatedClass ? '_tr.'.$property : $property;
                    $tab = 'builder*'.$property;
                    $transKey = sprintf($entityConfig['slug'].'.%s.builder.label', $property);
                    $catalogue = $this->translator->getCatalogue($this->translator->getLocale());
                    if (!$catalogue->defines($transKey)) {
                        $transKey = 'tab.builder.label.default';
                    }
                    $manipulator->addTab(
                        $entityFqcn,
                        'update',
                        $tab,
                        [
                            'label' => $transKey,
                            'icon' => 'fa fa-cogs',
                            'routeParams' => [
                                'tab' => $tab,
                            ],
                        ]
                    );
                }
            }
        }
    }

    protected function appendSeoTabs(ConfigManipulator $manipulator): void
    {
        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            $refl = new ReflectionClass($entityFqcn);
            $tabs = $entityConfig['tabs'] ?? [];
            if (
                empty($tabs['seo'])
                && (
                    // classical config: content + content_trans(containing SEO object)
                    ($refl->implementsInterface(SeoContentInterface::class) &&
                        $seoFqcn = $entityConfig['translation_class'])
                    // different config, if "content" class directly implements SEO trans
                    || ($refl->implementsInterface(SeoContentTranslationInterface::class) && $seoFqcn = $entityFqcn)
                )
            ) {
                $seoHolderRefl = new ReflectionClass($seoFqcn);

                if ($seoHolderRefl->implementsInterface(SeoContentTranslationInterface::class)) {
                    // add field
                    $action = 'update';
                    $manipulator->addTab(
                        $entityFqcn,
                        $action,
                        'seo',
                        [
                            'label' => 'seo.tab.label',
                            'icon' => 'fa fa-search',
                            'routeParams' => [
                                'tab' => 'seo',
                            ],
                        ]
                    );

                    // add field
                    $manipulator->addEntityFormField(
                        $entityFqcn,
                        $action,
                        $entityConfig['translation_class'] !== null ? '_tr.seoMetadata' : 'seoMetadata',
                        'SEO',
                        SeoMetadataType::class,
                        [],
                        'seo'
                    );
                }
            }
        }
    }

    protected function applyLinkedActions(ConfigManipulator $manipulator): void
    {
        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            foreach ($manipulator->getEntityActions($entityFqcn) as $action => $actionConfig) {
                if (!empty($actionConfig['type']) && $actionConfig['type'] === 'entity_action') {
                    if (empty(
                    $relatedActionConfig = $manipulator->getEntityAction(
                        $subEntityClass = $actionConfig['target_entity'],
                        $method = $actionConfig['target_action']
                    )
                    )) {
                        $this->error(
                            'No sub entity with class "%s" and action "%s" defined in config.',
                            [$subEntityClass, $method]
                        );
                    }
                    $actionParam = [
                        'route' => 'aa_admin_entity',
                    ];
                    if (!empty($relatedActionConfig['routeParamMap'])) {
                        $actionParam['routeParamMap'] = array_merge(
                            $action['routeParamMap'],
                            $relatedActionConfig['routeParamMap']
                        );
                    }
                    $manipulator->setEntityActionProps($entityFqcn, $action, $actionParam);
                    $relatedConfig = $manipulator->getEntityAction($subEntityClass, $method);
                    $manipulator->setEntityActionProp($entityFqcn, $action, 'target_config', $relatedConfig);
                }
            }
        }
    }

    protected function error(string $message, array $stringTokens = [])
    {
        if (stripos($message, '%s') !== false) {
            array_unshift($stringTokens, $message);
            $message = call_user_func_array('sprintf', $stringTokens);
        }

        throw new \Exception($message);
    }

    protected function normalizeControllers(ConfigManipulator $manipulator)
    {
        foreach ($manipulator->get('controllers') as $className => $config) {
            $slug = $this->createControllerSlug($className);

            if (empty($manipulator->get('controllers.'.$className.'.slug'))) {
                $manipulator->setControllerProp($className, 'slug', $slug);
            }

            if (empty($manipulator->get('controllers.'.$className.'.class'))) {
                $manipulator->setControllerProp($className, 'class', $className);
            }

            foreach ($config['actions'] ?? [] as $actionSlug => $actionConfig) {
                if (is_int($actionSlug)) {
                    unset($config['actions'][$actionSlug]);
                }
                $actionSlug = $actionConfig['action'];
                if (!array_key_exists($actionSlug, $config['actions'])) {
                    $config['actions'][$actionSlug] = $actionConfig;
                }

                $config['actions'][$actionSlug] = array_merge(
                    $actionConfig,
                    [
                        'method' => $this->caseConverter->kebabToCamelCase($actionSlug),
                    ]
                );
            }
            $manipulator->setControllerProp($className, 'actions', $config['actions']);
        }
    }

    protected function addExoticControllers(ConfigManipulator $manipulator)
    {
        // from entity
        foreach ($manipulator->get('entities') as $className => $entityConfig) {
            if (
                ($controller = $entityConfig['controller']) !== EntityController::class
                && !$manipulator->hasController($controller)
            ) {
                $manipulator->addController(
                    $entityConfig['controller'],
                    array_merge(
                        ConfigurationDefault::getDefaultControllerConfig(),
                        $manipulator->get('controllers.'.$entityConfig['controller']) ?? []
                    )
                );
            }
        }

        // from menu
        foreach ($manipulator->get('menu') as $k => $item) {
            if (empty($item) || empty($item['type'])) {
                throw new \Exception('Invalid admin configuration.');
            }
            if ($item['type'] === 'controller') {
                if (empty($controller = $item['controller']) || empty($method = $item['method'])) {
                    throw new RuntimeException(
                        'Error: controller and/or method not specified for "controller" type menu item.'
                    );
                } elseif ($controller === EntityController::class) {
                    throw new RuntimeException(
                        'Use classic "entity" menu item type when using default admin controller.'
                    );
                } elseif ($controller !== EntityController::class) {
                    if (!$manipulator->hasController($controller)) {
                        $manipulator->addController($controller);
                    }
                    $action = $this->caseConverter->camelToKebabCase($item['method']);
                    if (!$manipulator->get('controllers.'.$controller.'.actions.'.$action)) {
                        $manipulator->addControllerAction(
                            $item['controller'],
                            $action,
                            [
                                'action' => $action,
                                'method' => $item['method'],
                                'roles' => $item['roles'] ??
                                        $manipulator->get('controllers.'.$k.'.actions.'.$method.'.roles') ??
                                        $manipulator->get('controllers.'.$k.'.roles') ??
                                        $manipulator->get('global_roles'),
                            ]
                        );
                    }
                }
            }
        }
    }

    protected function processControllers(ConfigManipulator $manipulator)
    {
        foreach ($manipulator->get('controllers') as $k => $currentControllerCfg) {
            $refl = new ReflectionClass($k);
            $slug = $this->createControllerSlug($k);
            $class = $k;

            if (empty($manipulator->get('controllers.'.$k.'.slug'))) {
                $manipulator->setControllerProp($k, 'slug', $slug);
            }

            if (empty($manipulator->get('controllers.'.$k.'.class'))) {
                $manipulator->setControllerProp($k, 'class', $class);
            }

            // auto-add public methods as actions if "auto_actions" param is true
            if (true === $manipulator->getControllerProp($k, 'auto_actions')) {
                $controllerActions = [];
                foreach ($refl->getMethods(ReflectionMethod::IS_PUBLIC) as $methodRefl) {
                    $method = $methodRefl->getShortName();
                    if (
                        in_array(substr($method, 0, 2), ['__'])
                        || in_array(substr($method, 0, 3), ['get', 'set'])
                    ) {
                        continue;
                    }
                    $action = $this->caseConverter->camelToKebabCase($method);
                    $controllerActions[$action] = [
                        'action' => $action,
                        'roles' => $this->processRoleHierarchy(
                            $manipulator->get('global_roles'),
                            $currentControllerCfg['actions'][$method]['roles']
                            ?? $currentControllerCfg['roles']
                        ),
                    ];
                }
                $manipulator->setControllerProps($k, 'actions', $controllerActions);
            }

            $controllerSlugs = array_column($manipulator->get('controllers'), 'slug');
            if (count($controllerSlugs) != count(array_unique($controllerSlugs))) {
                $this->error(
                    'At least one controller slug is not unique. '.
                    'Make sure to manually define "slug" key to avoid any collision.'
                );
            }

        }
    }

    protected function processEntityRoles(ConfigManipulator $manipulator)
    {
        // global < entity < action
        $globalSecurityRole = $manipulator->get('global_roles');
        foreach ($manipulator->get('entities') as $className => $entityConfig) {
            $entityRoles = $entityConfig['roles'] ?? $globalSecurityRole;
            if (!is_array($entityRoles)) {
                throw new Exception('Invalid role format, you must specify an array');
            }
            $entityRoles = $this->processRoleHierarchy($globalSecurityRole, $entityRoles);

            foreach ($manipulator->getEntityProp($className, 'actions') as $k => $action) {
                // entity < action
                $actionRoles = $action['roles'] ?? $entityRoles;
                if (!is_array($actionRoles)) {
                    throw new Exception('Invalid role format, you must specify an array');
                }

                $roles = $this->processRoleHierarchy($entityRoles, $actionRoles);

                if (empty($roles)) {
                    $this->error(
                        'Empty roles computed for "%s", and action "%s". Check if the required role was removed for this action?',
                        [$className, $k]
                    );
                }

                $manipulator->setEntityActionProp($className, $k, 'roles', $roles);
            }
        }

        foreach ($manipulator->get('controllers') as $className => $entityConfig) {
            $entityRoles = $entityConfig['roles'] ?? $globalSecurityRole;
            if (!is_array($entityRoles)) {
                throw new Exception('Invalid role format, you must specify an array');
            }
            $entityRoles = $this->processRoleHierarchy($globalSecurityRole, $entityRoles);

            foreach ($entityConfig['actions'] as $k => $action) {
                // entity < action
                $actionRoles = $action['roles'] ?? $entityRoles;
                if (!is_array($actionRoles)) {
                    throw new Exception('Invalid role format, you must specify an array');
                }

                $roles = $this->processRoleHierarchy($entityRoles, $actionRoles);

                if (empty($roles)) {
                    $this->error(
                        'Empty roles computed for "%s", and action "%s". Check if the required role was removed for this action?',
                        [$className, $k]
                    );
                }

                $manipulator->setControllerProp($className, 'actions.'.$k.'.roles', $roles);
            }
        }

//        dump($manipulator->get('controllers'));
//        die;
    }

    protected function processEntityActionZones(ConfigManipulator $manipulator)
    {
        foreach ($manipulator->get('entities') as $className => $entityConfig) {
            // reindex actions using their slugs
            $entityActions = $manipulator->getEntityProp($className, 'actions');
            foreach ($entityActions as $idx => $actionCfg) {
                unset($entityActions[$idx]);
                $entityActions[$actionCfg['action']] = $actionCfg;
            }

            // set list as default if present
            if (empty($entityConfig['default_action']) && in_array('list', array_keys($entityActions))) {
                $manipulator->setEntityProp($className, 'default_action', 'list');
            }

            $manipulator->setEntityProp($className, 'actions', $entityActions);

            // add defaults when possible (configured value is kept as default, so null)
            $defaultsIfNull = [
                'topbar.actions' => [
                    'list',
                    'create',
                ],
                'list.item_actions' => [
                    'update',
                    'delete',
                ],
                'list.topbar' => [
                    'create',
                    'exportFields',
                ],
            ];
            foreach ($defaultsIfNull as $path => $defaultActions) {
                $source = $manipulator->getEntityProp($className, $path);
                if ($source === null) {
                    $defaultVal = [];
                    foreach ($defaultActions as $defaultAction) {
                        if (array_key_exists($defaultAction, $entityActions)) {
                            $defaultVal[] = $defaultAction;
                        }

                    }
                    $manipulator->setEntityProp($className, $path, $defaultVal);
                }
            }

            $entityActions = $manipulator->getEntityProp($className, 'actions');
            $entityActionSlugs = array_column($entityActions, 'action');


            // add all actions as root entry to be able to configure entity.N.actionSlug.actions[] array
            $actionZones = [];
            foreach ($entityActionSlugs as $entityActionSlug) {
                if (!array_key_exists($entityActionSlug, $actionZones)) {
                    $actionZones[] = $entityActionSlug.'.topbar';
                }
            }

            /*
             * inheritance (from topbar.actions to X.topbar)
             */
            $topBarConfig = $manipulator->getEntityProp($className, 'topbar.actions') ?? [];
            foreach ($actionZones as $path) {
                $zoneConfig = $manipulator->getEntityProp($className, $path);
                if ($zoneConfig === null) {
                    $zoneConfig = $topBarConfig;
                }
                $manipulator->setEntityProp($className, $path, $zoneConfig);
            }

            /*
             * normalize actions keys (topbar.actions, X.topbar, list.item_actions, etc)
             */
            // topbar.actions && list.item_actions && list.group_actions
            foreach (['topbar.actions', 'list.item_actions', 'list.group_actions'] as $path) {
                $source = $manipulator->getEntityProp($className, $path);
                if (!is_array($source)) {
                    continue;
                }
                $normedSource = $this->convertActionStringToConfigArray($source, $entityActions, $className);
                $manipulator->setEntityProp($className, $path, $normedSource);
            }
            // X.topbar
            foreach ($actionZones as $path) {
                $zoneConfig = $manipulator->getEntityProp($className, $path);
                $zoneConfig = $this->convertActionStringToConfigArray($zoneConfig, $entityActions, $className);
                $manipulator->setEntityProp($className, $path, $zoneConfig);
            }

            // if controller is not entity controller, then copy actions to this controller
            if (
                ($controllerClass = $manipulator->getEntityProp($className, 'controller'))
                && ($refl = new ReflectionClass($controllerClass))
                && !$refl->isSubclassOf(EntityController::class)
            ) {
                $manipulator->setControllerProp($controllerClass, 'actions', $entityActions);
            }
        }
    }

    /**
     * Convert [0 => 'abc',] to ['abc' => [...action config],]
     *
     * @param array $source
     * @param array $entityActions
     * @throws \Exception
     */
    protected function convertActionStringToConfigArray(array $source, array $entityActions, string $className)
    {
        if (empty($source)) {
            return [];
        }

        // filter existent actions
        $actions = array_intersect_key($entityActions, array_flip($source));
        // reorder actions using zone's order
        $ordered = [];
        foreach ($source as $action) {
            if (is_string($action)) {
                foreach ($actions as $actionCfg) {
                    if ($actionCfg['action'] === $action) {
                        $ordered[$action] = $actionCfg;
                        continue 2;
                    }
                }

                throw new UnknownActionException($className, $action);
            } else {
                $ordered[$action['action']] = $action;
            }
        }

        return $ordered;
    }

    protected function setupEntities(ConfigManipulator $manipulator)
    {
        $slugsInUse = [];
        foreach ($manipulator->get('entities') as $className => $entityConfig) {
            // slug
            if (empty($manipulator->getEntityProp($className, 'slug'))) {
                $slug = $this->slugger->slugify((new ReflectionClass($className))->getShortName());
                $slugsInUse[] = $slug;
                $manipulator->setEntityProp($className, 'slug', $slug);
            }

            // if no name, use slug
            if (empty($manipulator->getEntityProp($className, 'name'))) {
                $manipulator->setEntityProp($className, 'name', ucfirst($slug));
            }

            // double class name as attr
            if (empty($manipulator->getEntityProp($className, 'class'))) {
                $manipulator->setEntityProp($className, 'class', $className);
            }

            if (
                ($refl = new ReflectionClass($className)) && $refl->implementsInterface(TranslatableInterface::class)
            ) {
                $manipulator->setEntityProp($className, 'translation_class', $className.'Translation');
            }
        }

        if (count($slugsInUse) !== count(array_unique($slugsInUse))) {
            $this->error(
                'Duplicated slug encountered in configuration.'.
                ' Maybe two class names are too close, try to explicitly define "slug" property for your entities.'
            );
        }
    }

    protected function processMenu(ConfigManipulator $manipulator): void
    {
        foreach ($manipulator->get('menu') as $index => $config) {
            $type = $config['type'];
            if ($type === 'endsubmenu') {
                // informative item, do nothing
                continue;
            }

            if (empty($config['label'])) {
                if ($type === 'entity') {
                    $entityConfig = $manipulator->getEntity($config['class']);
//                    dump($manipulator->getConfig());
//                    dump($config['class']);
//                    dump($entityConfig);
                    if (empty($entityConfig)) {
                        throw new \Exception(sprintf('Could not find config for "%s" entity', $config ['class']));
                    }
//                    dump($index);
//                    dump($entityConfig['name']);
//                    die;
                    $manipulator->setMenuProp($index, 'label', $entityConfig['name']);
                } else {
                    $arrayString = str_replace("\n", "", print_r($config, true));
                    $this->error('Empty label given in item menu config: %s.', [$arrayString]);
                }
            }

            if (empty($config['route']) || empty($config['routeParameters'])) {
                $routeConfig = [
                    'route' => null,
                    'routeParameters' => [],
                ];
                if ($type === 'entity') {
                    $entityActionConfig = $manipulator->getEntityAction($config['class'], 'list');
                    if ($entityActionConfig === null) {
                        throw new \Exception(
                            sprintf('Could not resolve config for "%s" in admin menu. Invalid class?', $config['class'])
                        );
                    }
                    $routeConfig['route'] = $entityActionConfig['route'];
                    $routeConfig['routeParameters'] = $entityActionConfig['routeParameters'];
                    $manipulator->setMenuProps($index, $routeConfig);
                } elseif ($type === 'controller') {
                    $routeConfig['route'] = 'aa_admin_controller';
                    $routeConfig['routeParameters'] = [
                        'controller' => $this->createControllerSlug($config['controller']),
                        'method' => $config['method'],
                    ];
                    $manipulator->setMenuProps($index, $routeConfig);
                }
            }
        }
    }


    /**
     * Create a route parameter array with proper precedence and priorities for sorting
     *
     * @param $entityConfig
     * @param string $actionSlug
     * @param $actionConfig
     * @return array|string[]
     */
    private function handleSortingParameters($entityConfig, string $actionSlug, $actionConfig): array
    {
        $sortRouteParams = [];
        $trackedSorts = [];
        $fields = $entityConfig[$actionSlug]['fields'] ?? [];

        uasort(
            $fields,
            function ($a, $b) {
                $aPrio = 100;
                $bPrio = 100;

                if (!empty($a['sort'])) {
                    if (!empty($a['sort_index'])) {
                        $aPrio = $a['sort_index'];
                    } else {
                        $aPrio = 10;
                    }
                }

                if (!empty($b['sort'])) {
                    if (!empty($b['sort_index'])) {
                        $bPrio = $b['sort_index'];
                    } else {
                        $bPrio = 10;
                    }
                }

                if ($aPrio == $bPrio) {
                    return 0;
                }

                return ($aPrio < $bPrio) ? -1 : 1;
            }
        );

        foreach ($fields as $field) {
            if (!empty($field['sort']) && ($sort = $field['sort']) && in_array(strtoupper($sort), ['ASC', 'DESC'])) {
                $sortName = 'sort_'.$field['property'];
                $sortRouteParams[$sortName] = $sort;
                $trackedSorts[] = $sortName;
            }
        }

        // default sort by id DESC
        if (empty($trackedSorts)) {
            $sortRouteParams['sort_id'] = 'DESC';
            $trackedSorts[] = 'sort_id';
        }

        $sortRouteParams = array_merge(['sorting_order' => implode(',', $trackedSorts),], $sortRouteParams);

        return $sortRouteParams;
    }


    private function normalizeActions(ConfigManipulator $manipulator)
    {
        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            foreach ($manipulator->getEntityActions($entityFqcn) as $key => $action) {
                $actionParam = [];

                if ($action['attributes'] ?? []) {
                    // protected keys, used in action template
                    unset($action['attributes']['href']);
                    unset($action['attributes']['class']);
                    $actionParam['attributes'] = $action['attributes'];
                    $actionParam['attributesString'] = '';
                    foreach ($action['attributes'] as $k => $v) {
                        $actionParam['attributesString'] .= sprintf(' %s="%s"', $k, $v);
                    }
                }

                $manipulator->setEntityActionProps($entityFqcn, $key, $actionParam);
            }
        }
    }
}
