<?php

namespace AutoAdmin\Bundle\Admin\Configuration;

use AutoAdmin\Bundle\Admin\Attribute\Loader\AttributeLoader;
use AutoAdmin\Bundle\Admin\Attribute\PropertyAttributes;
use AutoAdmin\Bundle\Admin\Configuration\Enum\AdminTabValues;
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\BrickBuilder\Attributes\BrickBuilder;
use AutoAdmin\Bundle\Content\Breadcrumbs\Model\BreadcrumbsConfigInterface;
use AutoAdmin\Bundle\Content\Breadcrumbs\Model\BreadcrumbsConfigTranslationInterface;
use AutoAdmin\Bundle\Content\Model\TranslatableInterface;
use AutoAdmin\Bundle\Core\Slugger\Slugger;
use AutoAdmin\Bundle\Core\Util\CaseConverter;
use AutoAdmin\Bundle\Seo\Form\OpenGraphMetadataType;
use AutoAdmin\Bundle\Seo\Form\SchemaOrgMetadataType;
use AutoAdmin\Bundle\Seo\Form\SeoMetadataType;
use AutoAdmin\Bundle\Seo\Model\OpenGraphContentInterface;
use AutoAdmin\Bundle\Seo\Model\OpenGraphContentTranslationInterface;
use AutoAdmin\Bundle\Seo\Model\SchemaOrgContentInterface;
use AutoAdmin\Bundle\Seo\Model\SchemaOrgContentTranslationInterface;
use AutoAdmin\Bundle\Seo\Model\SeoContentInterface;
use AutoAdmin\Bundle\Seo\Model\SeoContentTranslationInterface;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use RuntimeException;
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\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);

        // assets
        $manipulator->addAsset('js', '/bundles/autoadmin/aa.admin.min.js', true); // admin bundle js
        $manipulator->addAsset('css', '/bundles/autoadmin/aa.admin.min.css', true); // admin bundle css
        $manipulator->addAsset('js', '/build/app_admin.min.js');// admin app js
        $manipulator->addAsset('css', '/build/app_admin.min.css');// admin app css

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

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

        // tabs
        $this->createTabs($manipulator);
        $this->appendBuilderTabs($manipulator);
        $this->appendSeoTabs($manipulator);
        $this->appendBreadcrumbsTabs($manipulator);

        // prepare "entity_action" typed actions by copying target information
        $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);

//        dump($config);
//        dump($config['entities']);
//        dump($config['entities'][array_key_first($config['entities'])]);
//        dump($config['entities'][array_key_first($config['entities'])]['form']['update']);
//        die;

        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;
                        // allow _list suffix for list-typed actions, avoid total freedom, less errors
                        $method = str_replace('_list', 'List', $method);
                        if (!$controllerRefl->hasMethod($method)) {
                            $this->error(
                                'No method "%s" in controller "%s" for entity "%s"',
                                [$method, $controller, $entityConfig['class'],]
                            );
                        }
                        $actionParam = [];
                        $actionParam['method'] = $method;
                        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) {
                                $typeOptions = [
                                    ...$guessed->getOptions(),
                                    ...($fieldConfig['type_options'] ?? [])
                                ];

                                $etConfig = $etGuess = null;

                                // apply guess to collection (guessing for entries)
                                if (
                                    !empty($fieldConfig['type'])
                                    && (
                                        $fieldConfig['type'] === 'collection'
                                        || str_ends_with($fieldConfig['type'], '_collection')
                                    )
                                    // shortcut string
                                    && is_string($et = $typeOptions['entry_type'])
                                    // not a class
                                    && !str_contains($et, '\\')
                                ) {
                                    $etConfig = [
                                        'type' => $typeOptions['entry_type'],
                                        'type_options' => $typeOptions['entry_options'] ?? [],
                                    ];
                                    $etGuess = $this->typeGuesserManager->guess($etConfig, $entityClass, $property);
                                    if ($etGuess) {
                                        $typeOptions = [
                                            ...$typeOptions,
                                            ...['entry_type' => $etGuess->getType()],
                                            ...[
                                                'entry_options' => [
                                                    ...$etGuess->getOptions(),
                                                    ...$etConfig['type_options'],
                                                ],
                                            ],
                                        ];
                                    }
                                }

                                $fieldConfig = array_merge(
                                    $fieldConfig,
                                    [
                                        'type' => $guessed->getType(),
                                        'type_options' => $typeOptions,
                                    ]
                                );
                            } 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 autoconfiguration 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'], 'fa fa-cog');
                        }
                    }
                }
            }
        }
    }

    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 = BrickBuilder::class;
                $builderAttributes = $attributes->getAttributesByType($builderAttr);

                /** @var PropertyAttributes $propertyAttributes */
                foreach ($builderAttributes as $property => $propertyAttributes) {
                    $property = $translatedClass && $class === $translatedClass ? '_tr.'.$property : $property;
                    $tab = 'builder*'.$property;
                    // permit to create custom builder tab and translate with key of type:
                    // "page.mybuilder.builder.label" in aa_admin domain
                    $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, 'fa fa-cogs', [
                        'label' => $transKey,
                    ]);
                }
            }
        }
    }

    protected function appendSeoTabs(ConfigManipulator $manipulator): void
    {
        $tabSlug = 'seo';
        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            $refl = new ReflectionClass($entityFqcn);
//            $tabs = $entityConfig['tabs'] ?? [];

            $tabsCfg = [
                AdminTabValues::SEO => [
                    'label' => 'tab.'.AdminTabValues::SEO.'.label',
                    'icon' => 'fa fa-search',
                    'routeParams' => [
                        'tab' => AdminTabValues::SEO,
                    ],
                ],
                AdminTabValues::STRUCTURED_DATA => [
                    'label' => 'tab.'.AdminTabValues::STRUCTURED_DATA.'.label',
                    'icon' => 'fa-regular fa-file-code',
                    'routeParams' => [
                        'tab' => AdminTabValues::STRUCTURED_DATA,
                    ],
                ],
            ];

            $dynamicTabConfigs = [
                [
                    'label' => 'SEO',
                    'interface' => SeoContentInterface::class,
                    'trans_interface' => SeoContentTranslationInterface::class,
                    'property_path' => 'seoMetadata',
                    'form_type' => SeoMetadataType::class,
                    'tab' => AdminTabValues::SEO,
                ],
                [
                    'label' => 'Schema.org',
                    'interface' => SchemaOrgContentInterface::class,
                    'trans_interface' => SchemaOrgContentTranslationInterface::class,
                    'property_path' => 'schemaOrgMetadata',
                    'form_type' => SchemaOrgMetadataType::class,
                    'tab' => AdminTabValues::STRUCTURED_DATA,
                ],
                [
                    'label' => 'OpenGraph',
                    'interface' => OpenGraphContentInterface::class,
                    'trans_interface' => OpenGraphContentTranslationInterface::class,
                    'property_path' => 'openGraphMetadata',
                    'form_type' => OpenGraphMetadataType::class,
                    'tab' => AdminTabValues::STRUCTURED_DATA,
                ],
            ];

            foreach ($dynamicTabConfigs as $cfg) {
                if (
                    // classical config: content + content_trans(containing SEO object)
                    ($refl->implementsInterface($cfg['interface']) &&
                        $currentFqcn = $entityConfig['translation_class'])
                    // different config, if "content" class directly implements SEO trans
                    || ($refl->implementsInterface(SeoContentTranslationInterface::class)
                        && $currentFqcn = $entityFqcn)
                ) {
                    $holderRefl = new ReflectionClass($currentFqcn);

                    if (!$manipulator->hasTab($entityFqcn, 'update', $tabName = $cfg['tab'])) {
                        $manipulator->addTab($entityFqcn, 'update', $tabName, null, $tabsCfg[$tabName]);
                    }

                    if ($holderRefl->implementsInterface(SeoContentTranslationInterface::class)) {
                        $manipulator->addEntityFormField(
                            $entityFqcn,
                            'update',
                            ($entityConfig['translation_class'] !== null ? '_tr.' : '').$cfg['property_path'],
                            $cfg['label'],
                            $cfg['form_type'],
                            [
                                'form_section' => $cfg['label'],
                            ],
                            $tabName
                        );
                    }
                }
            }
        }
    }

    protected function appendBreadcrumbsTabs(ConfigManipulator $manipulator): void
    {
        $tabSlug = AdminTabValues::ADVANCED;

        foreach ($manipulator->getEntities() as $entityFqcn => $entityConfig) {
            $refl = new ReflectionClass($entityFqcn);
            $tabs = $entityConfig['tabs'] ?? [];
            if (
                (
                    ($refl->implementsInterface(BreadcrumbsConfigInterface::class)
                        && $fqcn = $entityConfig['translation_class'])
                    || ($refl->implementsInterface(BreadcrumbsConfigTranslationInterface::class)
                        && $fqcn = $entityFqcn)
                )
            ) {
                $holderRefl = new ReflectionClass($fqcn);

                if ($holderRefl->implementsInterface(BreadcrumbsConfigTranslationInterface::class)) {
                    $action = 'update';
                    if (empty($tabs[$tabSlug])) {
                        $manipulator->addTab($entityFqcn, $action, $tabSlug, 'fa fa-ellipsis-h');
                    }

                    $labelPattern = $tabSlug.'.form.%s.label';
                    $cc = new CaseConverter();
                    $fields = [
                        ['property' => 'hiddenBreadcrumbs', 'type' => CheckboxType::class,],
                        ['property' => '_tr.breadcrumbsLabel', 'type' => TextType::class,],
                    ];
                    foreach ($fields as $field) {
                        $snakeLabel = $cc->camelToSnakeCase(str_replace('_tr.', '', $field['property']));
                        $label = sprintf($labelPattern, $snakeLabel);
                        $options = [];
                        $options['required'] = array_key_exists('required', $field) ? $field['required']: false;
                        $manipulator->addEntityFormField(
                            $entityFqcn,
                            $action,
                            $field['property'],
                            $label,
                            $field['type'],
                            $options,
                            $tabSlug
                        );
                    }
                }
            }
        }
    }

    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
            $normalizationPaths = ['topbar.actions', 'list.item_actions', 'list.group_actions'];

            // for list-type actions ("list" and "_list" suffixed actions)
            foreach ($entityActions as $a) {
                $aSlug = $a['action'];
                if (str_ends_with($aSlug, '_list')) {
                    $normalizationPaths[] = sprintf('%s.item_actions', $aSlug);
                    $normalizationPaths[] = sprintf('%s.group_actions', $aSlug);
                }
            }

            foreach ($normalizationPaths as $path) {
                $source = $manipulator->getEntityProp($className, $path);
                if (empty($source)) {
                    $source = [];
                }

                $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);
            }
        }
    }
}
