<?php

namespace Grav\Plugin\FlexObjects\Admin;

use Exception;
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Data;
use Grav\Common\Debugger;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Flex\Types\Pages\PageCollection;
use Grav\Common\Flex\Types\Pages\PageIndex;
use Grav\Common\Flex\Types\Pages\PageObject;
use Grav\Common\Grav;
use Grav\Common\Helpers\Excerpts;
use Grav\Common\Language\Language;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Framework\Controller\Traits\ControllerResponseTrait;
use Grav\Framework\File\Formatter\CsvFormatter;
use Grav\Framework\File\Formatter\YamlFormatter;
use Grav\Framework\File\Interfaces\FileFormatterInterface;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\FlexFormFlash;
use Grav\Framework\Flex\FlexObject;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
use Grav\Framework\Flex\Interfaces\FlexFormInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Flex\Interfaces\FlexTranslateInterface;
use Grav\Framework\Object\Interfaces\ObjectInterface;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Framework\Route\Route;
use Grav\Framework\Route\RouteFactory;
use Grav\Plugin\Admin\Admin;
use Grav\Plugin\FlexObjects\Controllers\MediaController;
use Grav\Plugin\FlexObjects\Flex;
use Nyholm\Psr7\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RocketTheme\Toolbox\Session\Message;
use RuntimeException;
use function dirname;
use function in_array;
use function is_array;
use function is_callable;

/**
 * Class AdminController
 * @package Grav\Plugin\FlexObjects
 */
class AdminController
{
    use ControllerResponseTrait;

    /** @var AdminController|null */
    private static $instance;

    /** @var Grav */
    public $grav;
    /** @var string */
    public $view;
    /** @var string */
    public $task;
    /** @var Route|null */
    public $route;
    /** @var array */
    public $post;
    /** @var array|null */
    public $data;

    /** @var array */
    protected $adminRoutes;
    /** @var Uri */
    protected $uri;
    /** @var Admin */
    protected $admin;
    /** @var UserInterface */
    protected $user;
    /** @var string */
    protected $redirect;
    /** @var int */
    protected $redirectCode;
    /** @var Route */
    protected $currentRoute;
    /** @var Route */
    protected $referrerRoute;

    /** @var string|null */
    protected $action;
    /** @var string|null */
    protected $location;
    /** @var string|null */
    protected $target;
    /** @var string|null */
    protected $id;
    /** @var bool */
    protected $active;
    /** @var FlexObjectInterface|false|null */
    protected $object;
    /** @var FlexCollectionInterface|null */
    protected $collection;
    /** @var FlexDirectoryInterface|null */
    protected $directory;

    /** @var string */
    protected $nonce_name = 'admin-nonce';
    /** @var string */
    protected $nonce_action = 'admin-form';
    /** @var string */
    protected $task_prefix = 'task';
    /** @var string */
    protected $action_prefix = 'action';

    /**
     * Unknown task, call onFlexTask[NAME] event.
     *
     * @return void
     */
    public function taskDefault(): void
    {
        $type = $this->target;
        $directory = $this->getDirectory($type);
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $object = $this->getObject();
        $key = $this->id;

        if ($object && $object->exists()) {
            $event = new Event(
                [
                    'type' => $type,
                    'key' => $key,
                    'admin' => $this->admin,
                    'flex' => $this->getFlex(),
                    'directory' => $directory,
                    'object' => $object,
                    'data' => $this->data,
                    'user' => $this->user,
                    'redirect' => $this->redirect
                ]
            );

            try {
                $this->grav->fireEvent('onFlexTask' . ucfirst($this->task), $event);
            } catch (Exception $e) {
                /** @var Debugger $debugger */
                $debugger = $this->grav['debugger'];
                $debugger->addException($e);
                $this->admin->setMessage($e->getMessage(), 'error');
            }

            $redirect = $event['redirect'];
            if ($redirect) {
                $this->setRedirect($redirect);
            }
        }
    }

    /**
     * Default action, onFlexAction[NAME] event.
     *
     * @return void
     */
    public function actionDefault(): void
    {
        $type = $this->target;
        $directory = $this->getDirectory($type);
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $object = $this->getObject();
        $key = $this->id;

        if ($object && $object->exists()) {
            $event = new Event(
                [
                    'type' => $type,
                    'key' => $key,
                    'admin' => $this->admin,
                    'flex' => $this->getFlex(),
                    'directory' => $directory,
                    'object' => $object,
                    'user' => $this->user,
                    'redirect' => $this->redirect
                ]
            );

            try {
                $this->grav->fireEvent('onFlexAction' . ucfirst($this->action), $event);
            } catch (Exception $e) {
                /** @var Debugger $debugger */
                $debugger = $this->grav['debugger'];
                $debugger->addException($e);
                $this->admin->setMessage($e->getMessage(), 'error');
            }

            $redirect = $event['redirect'];
            if ($redirect) {
                $this->setRedirect($redirect);
            }
        }
    }

    /**
     * Get datatable for list view.
     *
     * @return ResponseInterface|null
     */
    public function actionList(): ?ResponseInterface
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        // Check authorization.
        if (!$directory->isAuthorized('list', 'admin', $this->user)) {
            throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' list.', 403);
        }

        /** @var Uri $uri */
        $uri = $this->grav['uri'];
        if ($uri->extension() === 'json') {
            $options = [
                'collection' => $this->getCollection(),
                'url' => $uri->path(),
                'page' => $uri->query('page'),
                'limit' => $uri->query('per_page'),
                'sort' => $uri->query('sort'),
                'search' => $uri->query('filter'),
                'filters' => $uri->query('filters'),
            ];

            $table = $this->getFlex()->getDataTable($directory, $options);

            return $this->createJsonResponse($table->jsonSerialize());
        }

        return null;
    }

    /**
     * Alias for Export action.
     *
     * @return ResponseInterface|null
     */
    public function actionCsv(): ?ResponseInterface
    {
        return $this->actionExport();
    }

    /**
     * Export action. Defaults to CVS export.
     *
     * @return ResponseInterface|null
     */
    public function actionExport(): ?ResponseInterface
    {
        $collection = $this->getCollection();
        if (!$collection) {
            throw new RuntimeException('Not Found', 404);
        }

        // Check authorization.
        $directory = $collection->getFlexDirectory();
        $authorized = is_callable([$collection, 'isAuthorized'])
            ? $collection->isAuthorized('read', 'admin', $this->user)
            : $directory->isAuthorized('read', 'admin', $this->user);

        if (!$authorized) {
            throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' read.', 403);
        }

        $config = $collection->getFlexDirectory()->getConfig('admin.views.export') ?? $collection->getFlexDirectory()->getConfig('admin.export') ?? false;
        if (!$config || empty($config['enabled'])) {
            throw new RuntimeException($this->admin::translate('Not Found'), 404);
        }

        $queryParams = $this->getRequest()->getQueryParams();
        $type = $queryParams['type'] ?? null;
        if ($type) {
            $config = $config['options'][$type] ?? null;
            if (!$config) {
                throw new RuntimeException($this->admin::translate('Not Found'), 404);
            }
        }

        $defaultFormatter = CsvFormatter::class;
        $class = trim($config['formatter']['class'] ?? $defaultFormatter, '\\');
        $method = $config['method'] ?? ($class === $defaultFormatter ? 'csvSerialize' : 'jsonSerialize');
        if (!class_exists($class)) {
            throw new RuntimeException($this->admin::translate('Formatter Not Found'), 404);
        }
        /** @var FileFormatterInterface $formatter */
        $formatter = new $class($config['formatter']['options'] ?? []);
        $filename = ($config['filename'] ?? 'export') . $formatter->getDefaultFileExtension();

        if (method_exists($collection, $method)) {
            $list = $type ? $collection->{$method}($type) : $collection->{$method}();
        } else {
            $list = [];

            /** @var ObjectInterface $object */
            foreach ($collection as $object) {
                if (method_exists($object, $method)) {
                    $data = $object->{$method}();
                    if ($data) {
                        $list[] = $data;
                    }
                } else {
                    $list[] = $object->jsonSerialize();
                }
            }
        }

        $response = new Response(
            200,
            [
                'Content-Type' => $formatter->getMimeType(),
                'Content-Disposition' => 'inline; filename="' . $filename . '"',
                'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
                'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
                'Cache-Control' => 'no-store, no-cache, must-revalidate',
                'Pragma' => 'no-cache',
            ],
            $formatter->encode($list)
        );

        return $response;
    }

    /**
     * Delete object from directory.
     *
     * @return void
     */
    public function taskDelete(): void
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $object = null;
        try {
            $object = $this->getObject();
            if ($object && $object->exists()) {
                $authorized = $object instanceof FlexAuthorizeInterface
                    ? $object->isAuthorized('delete', 'admin', $this->user)
                    : $directory->isAuthorized('delete', 'admin', $this->user);

                if (!$authorized) {
                    throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' delete.', 403);
                }

                $object->delete();

                $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_DELETE_SUCCESS'));
                if ($this->currentRoute->withoutGravParams()->getRoute() === $this->referrerRoute->getRoute()) {
                    $redirect = dirname($this->currentRoute->withoutGravParams()->toString(true));
                } else {
                    $redirect = $this->referrerRoute->toString(true);
                }

                $this->setRedirect($redirect);

                $this->grav->fireEvent('onFlexAfterDelete', new Event(['type' => 'flex', 'object' => $object]));
            }
        } catch (RuntimeException $e) {
            $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_DELETE_FAILURE', $e->getMessage()]), 'error');

            $this->setRedirect($this->referrerRoute->toString(true), 302);
        }
    }

    /**
     * Create a new empty folder (from modal).
     *
     * TODO: Move pages specific logic
     *
     * @return void
     */
    public function taskSaveNewFolder(): void
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $collection = $directory->getIndex();
        if (!($collection instanceof PageCollection || $collection instanceof PageIndex)) {
            throw new RuntimeException('Task saveNewFolder works only for pages', 400);
        }

        $data = $this->data;
        $route = trim($data['route'] ?? '', '/');

        // TODO: Folder name needs to be validated! However we test against /="' as they are dangerous characters.
        $folder = mb_strtolower($data['folder'] ?? '');
        if ($folder === '' || preg_match('![="\']!u', $folder) !== 0) {
            throw new RuntimeException('Creating folder failed, bad folder name', 400);
        }

        $parent = $route ? $directory->getObject($route) : $collection->getRoot();
        if (!$parent instanceof PageObject) {
            throw new RuntimeException('Creating folder failed, bad parent route', 400);
        }

        if (!$parent->isAuthorized('create', 'admin', $this->user)) {
            throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' create.', 403);
        }

        $path = $parent->getFlexDirectory()->getStorageFolder($parent->getStorageKey());
        if (!$path) {
            throw new RuntimeException('Creating folder failed, bad parent storage path', 400);
        }

        // Ordering
        $orders = $parent->children()->visible()->getProperty('order');
        $maxOrder = 0;
        foreach ($orders as $order) {
            $maxOrder = max($maxOrder, (int)$order);
        }

        $orderOfNewFolder = $maxOrder ? sprintf('%02d.', $maxOrder+1) : '';
        $new_path         = $path . '/' . $orderOfNewFolder . $folder;

        /** @var UniformResourceLocator $locator */
        $locator = $this->grav['locator'];
        if ($locator->isStream($new_path)) {
            $new_path = $locator->findResource($new_path, true, true);
        } else {
            $new_path = GRAV_ROOT . '/' . $new_path;
        }

        Folder::create($new_path);
        Cache::clearCache('invalidate');
        $directory->getCache('index')->clear();

        $this->grav->fireEvent('onAdminAfterSaveAs', new Event(['path' => $new_path]));

        $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_NEW_FOLDER_SUCCESS'));

        $this->setRedirect($this->referrerRoute->toString(true));
    }

    /**
     * Create a new object (from modal).
     *
     * TODO: Move pages specific logic
     *
     * @return void
     */
    public function taskContinue(): void
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        if ($directory->getObject() instanceof PageInterface) {
            $this->continuePages($directory);
        } else {
            $this->continue($directory);
        }
    }

    /**
     * @param FlexDirectoryInterface $directory
     * @return void
     */
    protected function continue(FlexDirectoryInterface $directory): void
    {
        $config = $directory->getConfig('admin');
        $supported = !empty($config['modals']['add']);
        if (!$supported) {
            throw new RuntimeException('Task continue is not supported by the type', 400);
        }

        $authorized = $directory->isAuthorized('create', 'admin', $this->user);
        if (!$authorized) {
            $this->setRedirect($this->referrerRoute->toString(true));

            throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' add.', 403);
        }

        $this->object = $directory->createObject($this->data, '');

        // Reset form, we are starting from scratch.
        /** @var FlexForm $form */
        $form = $this->object->getForm('', ['reset' => true]);

        /** @var FlexFormFlash $flash */
        $flash = $form->getFlash();
        $flash->setUrl($this->getFlex()->adminRoute($this->object));
        $flash->save(true);

        $this->setRedirect($flash->getUrl());
    }

    /**
     * Create a new page (from modal).
     *
     * TODO: Move pages specific logic
     *
     * @return void
     */
    protected function continuePages(FlexDirectoryInterface $directory): void
    {

        $this->data['route'] = '/' . trim($this->data['route'] ?? '', '/');
        $route = trim($this->data['route'], '/');

        if ($route) {
            $parent = $directory->getObject($route);
        } else {
            // Use root page or fail back to directory auth.
            $index = $directory->getIndex();
            $parent = $index->getRoot() ?? $directory;
        }
        $authorized = $parent instanceof FlexAuthorizeInterface
            ? $parent->isAuthorized('create', 'admin', $this->user)
            : $directory->isAuthorized('create', 'admin', $this->user);

        if (!$authorized) {
            $this->setRedirect($this->referrerRoute->toString(true));

            throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' add.', 403);
        }

        $folder = $this->data['folder'] ?? null;
        $title = $this->data['title'] ?? null;
        if ($title) {
            $this->data['header']['title'] = $this->data['title'];
            unset($this->data['title']);
        }
        if (null !== $folder && 0 === strpos($folder, '@slugify-')) {
            $folder = \Grav\Plugin\Admin\Utils::slug($this->data[substr($folder, 9)] ?? '');
        }
        if (!$folder) {
            $folder = \Grav\Plugin\Admin\Utils::slug($title) ?: '';
        }
        $folder = ltrim($folder, '_');
        if ($folder === '' || mb_strpos($folder, '/') !== false) {
            throw new RuntimeException('Creating page failed: bad folder name', 400);
        }

        if (!isset($this->data['name'])) {
            // Get default child type.
            $this->data['name'] = $parent->header()->child_type ?? $parent->getBlueprint()->child_type ?? 'default';
        }
        if (strpos($this->data['name'], 'modular/') === 0) {
            $this->data['header']['body_classes'] = 'modular';
            $folder = '_' . $folder;
        }
        $this->data['folder'] = $folder;

        unset($this->data['blueprint']);
        $key = trim("{$route}/{$folder}", '/');
        if ($directory->getObject($key)) {
            throw new RuntimeException("Page '/{$key}' already exists!", 403);
        }

        $max = 0;
        if (isset($this->data['visible'])) {
            $auto = $this->data['visible'] === '';
            $visible = (bool)($this->data['visible'] ?? false);
            unset($this->data['visible']);

            // Empty string on visible means auto.
            if ($auto || $visible) {
                $children = $parent ? $parent->children()->visible() : [];
                $max = $auto ? 0 : 1;
                foreach ($children as $child) {
                    $max = max($max, (int)$child->order());
                }
            }

            $this->data['order'] = $max ? $max + 1 : false;
        }

        $this->data['lang'] = $this->getLanguage();

        $header = $this->data['header'] ?? [];
        $this->grav->fireEvent('onAdminCreatePageFrontmatter', new Event(['header' => &$header,
            'data' => $this->data]));

        $formatter = new YamlFormatter();
        $this->data['frontmatter'] = $formatter->encode($header);
        $this->data['header'] = $header;

        $this->object = $directory->createObject($this->data, $key);

        // Reset form, we are starting from scratch.
        /** @var FlexForm $form */
        $form = $this->object->getForm('', ['reset' => true]);

        /** @var FlexFormFlash $flash */
        $flash = $form->getFlash();
        $flash->setUrl($this->getFlex()->adminRoute($this->object));
        $flash->save(true);

        // Store the name and route of a page, to be used pre-filled defaults of the form in the future
        $this->admin->session()->lastPageName  = $this->data['name'] ?? '';
        $this->admin->session()->lastPageRoute = $this->data['route'] ?? '';

        $this->setRedirect($flash->getUrl());
    }

    /**
     * Save page as a new copy.
     *
     * Route: /pages
     *
     * @return void
     * @throws RuntimeException
     */
    protected function taskCopy(): void
    {
        try {
            $directory = $this->getDirectory();
            if (!$directory) {
                throw new RuntimeException('Not Found', 404);
            }

            $object = $this->getObject();
            if (!$object || !$object->exists() || !is_callable([$object, 'createCopy'])) {
                throw new RuntimeException('Not Found', 404);
            }

            // Pages are a special case.
            $parent = $object instanceof PageInterface ? $object->parent() : $object;
            $authorized = $parent instanceof FlexAuthorizeInterface
                ? $parent->isAuthorized('create', 'admin', $this->user)
                : $directory->isAuthorized('create', 'admin', $this->user);

            if (!$authorized || !$parent) {
                throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' copy.',
                    403);
            }

            if ($object instanceof PageInterface && is_array($this->data)) {
                $data = $this->data;
                $blueprints = $this->admin->blueprints('admin/pages/move');
                $blueprints->validate($data);
                $data = $blueprints->filter($data, true, true);
                // Hack for pages
                $data['name'] = $data['name'] ?? $object->template();
                $data['ordering'] = (int)$object->order() > 0;
                $data['order'] = null;
                if (isset($data['title'])) {
                    $data['header']['title'] = $data['title'];
                    unset($data['title']);
                }

                $object->order(false);
                $object->update($data);
            }

            $object = $object->createCopy();

            $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_COPY_SUCCESS'));

            $this->setRedirect($this->getFlex()->adminRoute($object));

        } catch (RuntimeException $e) {
            $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_COPY_FAILURE', $e->getMessage()]), 'error');
            $this->setRedirect($this->referrerRoute->toString(true), 302);
        }
    }

    /**
     * $data['route'] = $this->grav['uri']->param('route');
     * $data['sortby'] = $this->grav['uri']->param('sortby', null);
     * $data['filters'] = $this->grav['uri']->param('filters', null);
     * $data['page'] $this->grav['uri']->param('page', true);
     * $data['base'] = $this->grav['uri']->param('base');
     * $initial = (bool) $this->grav['uri']->param('initial');
     *
     * @return ResponseInterface
     * @throws RequestException
     * @TODO: Move pages specific logic
     */
    protected function actionGetLevelListing(): ResponseInterface
    {
        /** @var PageInterface|FlexObjectInterface $object */
        $object = $this->getObject($this->id ?? '');

        if (!$object || !method_exists($object, 'getLevelListing')) {
            throw new RuntimeException('Not Found', 404);
        }

        $request = $this->getRequest();
        $data = $request->getParsedBody();

        if (!isset($data['field'])) {
            throw new RequestException($request, 'Bad Request', 400);
        }

        // Base64 decode the route
        $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
        $data['filters'] = json_decode($data['filters'] ?? '{}', true, 512, JSON_THROW_ON_ERROR) + ['type' => ['root', 'dir']];

        $initial = $data['initial'] ?? null;
        if ($initial) {
            $data['leaf_route'] = $data['route'];
            $data['route'] = null;
            $data['level'] = 1;
        }

        [$status, $message, $response,$route] = $object->getLevelListing($data);

        $json = [
            'status'  => $status,
            'message' => $this->admin::translate($message ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'),
            'route' => $route,
            'initial' => (bool)$initial,
            'data' => array_values($response)
        ];

        return $this->createJsonResponse($json, 200);
    }

    /**
     * $data['route'] = $this->grav['uri']->param('route');
     * $data['sortby'] = $this->grav['uri']->param('sortby', null);
     * $data['filters'] = $this->grav['uri']->param('filters', null);
     * $data['page'] $this->grav['uri']->param('page', true);
     * $data['base'] = $this->grav['uri']->param('base');
     * $initial = (bool) $this->grav['uri']->param('initial');
     *
     * @return ResponseInterface
     * @throws RequestException
     * @TODO: Move pages specific logic
     */
    protected function actionListLevel(): ResponseInterface
    {
        try {
            /** @var PageInterface|FlexObjectInterface $object */
            $object = $this->getObject('');

            if (!$object || !method_exists($object, 'getLevelListing')) {
                throw new RuntimeException('Not Found', 404);
            }

            $directory = $object->getFlexDirectory();
            if (!$directory->isAuthorized('list', 'admin', $this->user)) {
                throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' getLevelListing.',
                    403);
            }

            $request = $this->getRequest();
            $data = $request->getParsedBody();

            // Base64 decode the route
            $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
            $data['filters'] = ($data['filters'] ?? []) + ['type' => ['dir']];
            $data['lang'] = $this->getLanguage();

            // Display root if permitted.
            $action = $directory->getConfig('admin.views.configure.authorize') ?? $directory->getConfig('admin.configure.authorize') ?? 'admin.super';
            if ($this->user->authorize($action)) {
                $data['filters']['type'][] = 'root';
            }

            $initial = $data['initial'] ?? null;
            if ($initial) {
                $data['leaf_route'] = $data['route'];
                $data['route'] = null;
                $data['level'] = 1;
            }

            [$status, $message, $response, $route] = $object->getLevelListing($data);

            $json = [
                'status'  => $status,
                'message' => $this->admin::translate($message ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'),
                'route' => $route,
                'initial' => (bool)$initial,
                'data' => array_values($response)
            ];
        } catch (Exception $e) {
            return $this->createErrorResponse($e);
        }

        return $this->createJsonResponse($json, 200);
    }

    /**
     * @return ResponseInterface
     */
    public function taskReset(): ResponseInterface
    {
        $key = $this->id;

        $object = $this->getObject($key);
        if (!$object) {
            throw new RuntimeException('Not Found', 404);
        }

        /** @var FlexForm $form */
        $form = $this->getForm($object);
        $form->getFlash()->delete();

        return $this->createRedirectResponse($this->referrerRoute->toString(true));
    }

    /**
     * @return void
     */
    public function taskSaveas(): void
    {
        $this->taskSave();
    }

    /**
     * @return void
     */
    public function taskSave(): void
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $key = $this->id;

        try {
            $object = $this->getObject($key);
            if (!$object) {
                throw new RuntimeException('Not Found', 404);
            }

            $authorized = $object instanceof FlexAuthorizeInterface
                ? $object->isAuthorized('save', 'admin', $this->user)
                : $directory->isAuthorized($object->exists() ? 'update' : 'create', 'admin', $this->user);

            if (!$authorized) {
                throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.',
                        403);
            }

            /** @var ServerRequestInterface $request */
            $request = $this->grav['request'];

            /** @var FlexForm $form */
            $form = $this->getForm($object);

            $callable = function (array $data, array $files, FlexObject $object) use ($form) {
                if (method_exists($object, 'storeOriginal')) {
                    $object->storeOriginal();
                }
                $object->update($data, $files);

                // Support for expert mode.
                if (str_ends_with($form->getId(), '-raw') && isset($data['frontmatter']) && is_callable([$object, 'frontmatter'])) {
                    if (!$this->user->authorize('admin.super')) {
                        throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save raw.',
                        403);
                    }
                    $object->frontmatter($data['frontmatter']);
                    unset($data['frontmatter']);
                }

                if (is_callable([$object, 'check'])) {
                    $object->check($this->user);
                }

                $object->save();
            };

            $form->setSubmitMethod($callable);
            $form->handleRequest($request);
            $error = $form->getError();
            $errors = $form->getErrors();
            if ($errors) {
                if ($error) {
                    $this->admin->setMessage($error, 'error');
                }

                foreach ($errors as $field => $list) {
                    foreach ((array)$list as $message) {
                        $this->admin->setMessage($message, 'error');
                    }
                }
                throw new RuntimeException('Form validation failed, please check your input');
            }
            if ($error) {
                throw new RuntimeException($error);
            }

            $object = $form->getObject();
            $objectKey = $object->getKey();

            $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_SAVE_SUCCESS'));

            // Set route to point to the current page.
            if (!$this->redirect) {
                $postAction = $request->getParsedBody()['_post_entries_save'] ?? 'edit';
                $this->grav['session']->post_entries_save = $postAction;
                if ($postAction === 'create-new') {
                    // Create another.
                    $route = $this->referrerRoute->withGravParam('action', null)->withGravParam('', 'add');
                } elseif ($postAction === 'list') {
                    // Back to listing.
                    $route = $this->currentRoute;

                    // Remove :add action.
                    $actionAdd = $key === '' || $route->getGravParam('action') === 'add' || $route->getGravParam('') === 'add';
                    if ($actionAdd) {
                        $route = $route->withGravParam('action', null)->withGravParam('', null);
                    }

                    $len = ($key === '' ? 0 : -1) - \substr_count($key, '/');
                    if ($len) {
                        $route = $route->withRoute($route->getRoute(0, $len));
                    }
                } else {
                    // Back to edit.
                    $route = $this->currentRoute;
                    $isRoot = $object instanceof PageInterface && $object->root();
                    $hasKeyChanged = !$isRoot && $key !== $objectKey;

                    // Remove :add action.
                    $actionAdd = $key === '' || $route->getGravParam('action') === 'add' || $route->getGravParam('') === 'add';
                    if ($actionAdd) {
                        $route = $route->withGravParam('action', null)->withGravParam('', null);
                    }

                    if ($hasKeyChanged) {
                        if ($key === '') {
                            // Append new key.
                            $path = $route->getRoute() . '/' . $objectKey;
                        } elseif ($objectKey === '') {
                            // Remove old key.
                            $path = preg_replace('|/' . preg_quote($key, '|') . '$|u', '/', $route->getRoute());
                        } else {
                            // Replace old key with new key.
                            $path = preg_replace('|/' . preg_quote($key, '|') . '$|u', '/' . $objectKey, $route->getRoute());
                        }
                        $route = $route->withRoute($path);
                    }

                    // Make sure we're using the correct language.
                    $lang = null;
                    if ($object instanceof FlexTranslateInterface) {
                        $lang = $object->getLanguage();
                        $route = $route->withLanguage($lang);
                    }
                }

                $this->setRedirect($route->toString(true));
            }

            $this->grav->fireEvent('onFlexAfterSave', new Event(['type' => 'flex', 'object' => $object]));
        } catch (RuntimeException $e) {
            $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_SAVE_FAILURE', $e->getMessage()]), 'error');

            if (isset($object, $form)) {
                $data = $form->getData();
                if (null !== $data) {
                    $flash = $form->getFlash();
                    $flash->setObject($object);
                    if ($data instanceof Data) {
                        $flash->setData($data->toArray());
                    }
                    $flash->save();
                }
            }

            // $this->setRedirect($this->referrerRoute->withQueryParam('uid', $flash->getUniqueId())->toString(true), 302);
            $this->setRedirect($this->referrerRoute->toString(true), 302);
        }
    }

    /**
     * @return void
     */
    public function taskConfigure(): void
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        try {
            $config = $directory->getConfig('admin.views.configure.authorize') ?? $directory->getConfig('admin.configure.authorize') ?? 'admin.super';
            if (!$this->user->authorize($config)) {
                throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' configure.', 403);
            }

            /** @var ServerRequestInterface $request */
            $request = $this->grav['request'];

            /** @var FlexForm $form */
            $form = $this->getDirectoryForm();
            $form->handleRequest($request);
            $error = $form->getError();
            $errors = $form->getErrors();
            if ($errors) {
                if ($error) {
                    $this->admin->setMessage($error, 'error');
                }

                foreach ($errors as $field => $list) {
                    foreach ((array)$list as $message) {
                        $this->admin->setMessage($message, 'error');
                    }
                }
                throw new RuntimeException('Form validation failed, please check your input');
            }
            if ($error) {
                throw new RuntimeException($error);
            }

            $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_CONFIGURE_SUCCESS'));

            if (!$this->redirect) {
                $this->referrerRoute = $this->currentRoute;

                $this->setRedirect($this->referrerRoute->toString(true));
            }
        } catch (RuntimeException $e) {
            $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_CONFIGURE_FAILURE', $e->getMessage()]), 'error');
            $this->setRedirect($this->referrerRoute->toString(true), 302);
        }
    }

    /**
     * Used in 3rd party editors (e.g. next-gen).
     *
     * @return ResponseInterface
     */
    public function actionConvertUrls(): ResponseInterface
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $key = $this->id;

        $object = $this->getObject($key);
        if (!$object instanceof PageInterface) {
            throw new RuntimeException('Not Found', 404);
        }

        $authorized = $object instanceof FlexAuthorizeInterface
            ? $object->isAuthorized('read', 'admin', $this->user)
            : $directory->isAuthorized($object->exists() ? 'read' : 'create', 'admin', $this->user);

        if (!$authorized) {
            throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.',
                403);
        }

        $request = $this->getRequest();
        $data = $request->getParsedBody();

        $data['data'] = json_decode($data['data'] ?? '{}', true, 512, JSON_THROW_ON_ERROR);
        if (!isset($data['data'])) {
            throw new RequestException($request, 'Bad Request', 400);
        }

        $converted_links = [];
        foreach ($data['data']['a'] ?? [] as $link) {
            $converted_links[$link] = Excerpts::processLinkHtml($link, $object);
        }

        $converted_images = [];
        foreach ($data['data']['img'] ?? [] as $image) {
            $converted_images[$image] = Excerpts::processImageHtml($image, $object);
        }

        $json = [
            'status'  => 'success',
            'message' => 'All links converted',
            'data' => ['links' => $converted_links, 'images' => $converted_images]
        ];

        return $this->createJsonResponse($json, 200);
    }

    /**
     * @return ResponseInterface
     */
    public function taskMediaList(): ResponseInterface
    {
        return $this->forwardMediaTask('action', 'media.list');
    }

    /**
     * @return ResponseInterface
     */
    public function taskMediaUpload(): ResponseInterface
    {
        return $this->forwardMediaTask('task', 'media.upload');
    }

    /**
     * @return ResponseInterface
     */
    public function taskMediaUploadMeta(): ResponseInterface
    {
        return $this->forwardMediaTask('task', 'media.upload.meta');
    }

    /**
     * @return ResponseInterface
     */
    public function taskMediaReorder(): ResponseInterface
    {
        return $this->forwardMediaTask('task', 'media.reorder');
    }

    /**
     * @return ResponseInterface
     */
    public function taskMediaDelete(): ResponseInterface
    {
        return $this->forwardMediaTask('task', 'media.delete');
    }

    /**
     * @return ResponseInterface
     */
    public function taskListmedia(): ResponseInterface
    {
        return $this->taskMediaList();
    }

    /**
     * @return ResponseInterface
     */
    public function taskAddmedia(): ResponseInterface
    {
        return $this->forwardMediaTask('task', 'media.copy');
    }

    /**
     * @return ResponseInterface
     */
    public function taskDelmedia(): ResponseInterface
    {
        return $this->forwardMediaTask('task', 'media.remove');
    }

    /**
     * @return ResponseInterface
     * @deprecated Do not use
     */
    public function taskFilesUpload(): ResponseInterface
    {
        throw new RuntimeException('Task filesUpload should not be called!');
    }

    /**
     * @param string|null $filename
     * @return ResponseInterface
     * @deprecated Do not use
     */
    public function taskRemoveMedia($filename = null): ResponseInterface
    {
        throw new RuntimeException('Task removeMedia should not be called!');
    }

    /**
     * @return ResponseInterface
     */
    public function taskGetFilesInFolder(): ResponseInterface
    {
        return $this->forwardMediaTask('action', 'media.picker');
    }

    /**
     * @param string $type
     * @param string $name
     * @return ResponseInterface
     */
    protected function forwardMediaTask(string $type, string $name): ResponseInterface
    {
        $directory = $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $route = Uri::getCurrentRoute()->withGravParam('task', null);
        $object = $this->getObject();

        /** @var ServerRequest $request */
        $request = $this->grav['request'];
        $request = $request
            ->withAttribute($type, $name)
            ->withAttribute('type', $this->target)
            ->withAttribute('key', $this->id)
            ->withAttribute('storage_key', $object && $object->exists() ? $object->getStorageKey() : null)
            ->withAttribute('route', $route)
            ->withAttribute('forwarded', true)
            ->withAttribute('object', $object);

        $controller = new MediaController();
        $controller->setUser($this->user);

        return $controller->handle($request);
    }

    /**
     * @return Flex
     */
    protected function getFlex(): Flex
    {
        return Grav::instance()['flex_objects'];
    }

    public static function getInstance(): ?AdminController
    {
        return self::$instance;
    }

    /**
     * AdminController constructor.
     */
    public function __construct()
    {
        self::$instance = $this;

        $this->grav = Grav::instance();
        $this->admin = $this->grav['admin'];
        $this->user = $this->admin->user;

        $this->active = false;

        // Controller can only be run in admin.
        if (!Utils::isAdminPlugin()) {
            return;
        }

        [, $location, $target] = $this->grav['admin']->getRouteDetails();
        if (!$location) {
            return;
        }
        $target = \is_string($target) ? urldecode($target) : null;

        /** @var Uri $uri */
        $uri = $this->grav['uri'];
        $routeObject = $uri::getCurrentRoute();
        $routeObject->withExtension('');

        $routes = $this->getAdminRoutes();

        // Match route to the flex directory.
        $path = '/' . ($target ? $location . '/' . $target : $location) . '/';
        $test = $routes[$path] ?? null;

        $directory = null;
        if ($test)  {
            $directory = $test['directory'];
            $location = trim($path, '/');
            $target = '';
        } else {
            krsort($routes);
            foreach ($routes as $route => $test) {
                if (strpos($path, $route) === 0) {
                    $directory = $test['directory'];
                    $location = trim($route, '/');
                    $target = trim(substr($path, strlen($route)), '/');
                    break;
                }
                $test = null;
            }
        }

        if ($directory) {
            // Redirect aliases.
            if (isset($test['redirect'])) {
                $route = $test['redirect'];
                // If directory route starts with alias and path continues, stop.
                if ($target && strpos($route, $location) === 0) {
                    // We are not in a directory.
                    return;
                }
                $redirect = '/' . $route . ($target ? '/' . $target : '');
                $this->setRedirect($redirect, 302);
                $this->redirect();
            } elseif (isset($test['action'])) {
                $routeObject = $routeObject->withGravParam('', $test['action']);
            }

            $id = $target;
            $target = $directory->getFlexType();
        } else {
            // We are not in a directory.
            if ($location !== 'flex-objects') {
                return;
            }
            $array = explode('/', $target, 2);
            $target = array_shift($array) ?: null;
            $id = array_shift($array) ?: null;
        }

        // Post
        $post = $_POST;
        if (isset($post['data'])) {
            $data = $post['data'];
            if (is_string($data)) {
                $data = json_decode($data, true);
            }
            $this->data = $this->getPost($data);
            unset($post['data']);
        }

        // Task
        $task = $this->grav['task'];
        if ($task) {
            $this->task = $task;
        }

        $this->post = $this->getPost($post);
        $this->location = 'flex-objects';
        $this->target = $target;
        $this->id = $this->post['id'] ?? $id;
        $this->action = $this->post['action'] ?? $uri->param('action', null) ?? $uri->param('', null) ?? $routeObject->getGravParam('');
        $this->active = true;
        $this->currentRoute = $uri::getCurrentRoute();
        $this->route = $routeObject;

        $base = $this->grav['pages']->base();
        if ($base) {
            // Fix referrer for sub-folder multi-site setups.
            $referrer = preg_replace('`^' . $base . '`', '', $uri->referrer());
        } else {
            $referrer = $uri->referrer();
        }
        $this->referrerRoute = $referrer ? RouteFactory::createFromString($referrer) : $this->currentRoute;
    }

    public function getInfo(): array
    {
        if (!$this->isActive()) {
            return [];
        }

        $class = AdminController::class;
        return [
            'controller' => [
                'name' => $this->location,
                'instance' => [$class, 'getInstance']
            ],
            'location' => $this->location,
            'type' => $this->target,
            'key' => $this->id,
            'action' => $this->action,
            'task' => $this->task
        ];
    }

    /**
     * Performs a task or action on a post or target.
     *
     * @return ResponseInterface|bool|null
     */
    public function execute()
    {
        if (!$this->user->authorize('admin.login')) {
            // TODO: improve
            return false;
        }

        $params = [];

        $event = new Event(
            [
                'type' => &$this->target,
                'key' => &$this->id,
                'directory' => &$this->directory,
                'collection' => &$this->collection,
                'object' => &$this->object
            ]
        );
        $this->grav->fireEvent("flex.{$this->target}.admin.route", $event);

        if ($this->isFormSubmit()) {
            $form = $this->getForm();
            $this->nonce_name = $form->getNonceName();
            $this->nonce_action = $form->getNonceAction();
        }

        // Handle Task & Action
        if ($this->task) {
            // validate nonce
            if (!$this->validateNonce()) {
                $e = new RequestException($this->getRequest(), 'Page Expired', 400);

                $this->close($this->createErrorResponse($e));
            }
            $method = $this->task_prefix . ucfirst(str_replace('.', '', $this->task));

            if (!method_exists($this, $method)) {
                $method = $this->task_prefix . 'Default';
            }

        } elseif ($this->target) {
            if (!$this->action) {
                if ($this->id) {
                    $this->action = 'edit';
                    $params[] = $this->id;
                } else {
                    $this->action = 'list';
                }
            }
            $method = 'action' . ucfirst(strtolower(str_replace('.', '', $this->action)));

            if (!method_exists($this, $method)) {
                $method = $this->action_prefix . 'Default';
            }
        } else {
            return null;
        }

        if (!method_exists($this, $method)) {
            return null;
        }

        try {
            $response = $this->{$method}(...$params);
        } catch (RequestException $e) {
            $response = $this->createErrorResponse($e);
        } catch (RuntimeException $e) {
            // If task fails to run, redirect back to the previous page and display the error message.
            if ($this->task && !$this->redirect) {
                $this->setRedirect($this->referrerRoute->toString(true));
            }
            $response = null;
            $this->setMessage($e->getMessage(), 'error');
        }

        if ($response instanceof ResponseInterface) {
            $this->close($response);
        }

        // Grab redirect parameter.
        $redirect = $this->post['_redirect'] ?? null;
        unset($this->post['_redirect']);

        // Redirect if requested.
        if ($redirect) {
            $this->setRedirect($redirect);
        }

        return $response;
    }

    /**
     * @return bool
     */
    public function isFormSubmit(): bool
    {
        return (bool)($this->post['__form-name__'] ?? null);
    }

    /**
     * @param FlexObjectInterface|null $object
     * @return FlexFormInterface
     */
    public function getForm(FlexObjectInterface $object = null): FlexFormInterface
    {
        $object = $object ?? $this->getObject();
        if (!$object) {
            throw new RuntimeException('Not Found', 404);
        }

        $formName = $this->post['__form-name__'] ?? '';
        $name = '';
        $uniqueId = null;

        // Get the form name. This defines the blueprint which is being used.
        if (strpos($formName, '-')) {
            $parts = explode('-', $formName);
            $prefix = $parts[0] ?? '';
            $type = $parts[1] ?? '';
            if ($prefix === 'flex' && $type === $object->getFlexType()) {
                $name = $parts[2] ?? '';
                if ($name === 'object') {
                    $name = '';
                }
                $uniqueId = $this->post['__unique_form_id__'] ?? null;
            }
        }

        $options = [
            'unique_id' => $uniqueId,
        ];

        return $object->getForm($name, $options);
    }

    /**
     * @param FlexDirectoryInterface|null $directory
     * @return FlexFormInterface
     */
    public function getDirectoryForm(FlexDirectoryInterface $directory = null): FlexFormInterface
    {
        $directory = $directory ?? $this->getDirectory();
        if (!$directory) {
            throw new RuntimeException('Not Found', 404);
        }

        $formName = $this->post['__form-name__'] ?? '';
        $name = '';
        $uniqueId = null;

        // Get the form name. This defines the blueprint which is being used.
        if (strpos($formName, '-')) {
            $parts = explode('-', $formName);
            $prefix = $parts[0] ?? '';
            $type = $parts[1] ?? '';
            if ($prefix === 'flex_conf' && $type === $directory->getFlexType()) {
                $name = $parts[2] ?? '';
                $uniqueId = $this->post['__unique_form_id__'] ?? null;
            }
        }

        $options = [
            'unique_id' => $uniqueId,
        ];

        return $directory->getDirectoryForm($name, $options);
    }

    /**
     * @param string|null $key
     * @return FlexObjectInterface|null
     */
    public function getObject(string $key = null): ?FlexObjectInterface
    {
        if (null === $this->object) {
            $key = $key ?? $this->id;
            $object = false;

            $directory = $this->getDirectory();
            if ($directory) {
                // FIXME: hack for pages.
                if ($key === '_root') {
                    $index = $directory->getIndex();
                    if ($index instanceof PageIndex) {
                        $object = $index->getRoot();
                    }
                } elseif (null !== $key) {
                    $object = $directory->getObject($key) ?? $directory->createObject([], $key);
                } elseif ($this->action === 'add') {
                    $object = $directory->createObject([], '');
                }

                if ($object instanceof FlexTranslateInterface && $this->isMultilang()) {
                    $language = $this->getLanguage();
                    if ($object->hasTranslation($language)) {
                        $object = $object->getTranslation($language);
                    } elseif (!in_array('', $object->getLanguages(true), true)) {
                        $object->language($language);
                    }
                }

                if (is_callable([$object, 'refresh'])) {
                    $object->refresh();
                }

                // Get updated object via form.
                $this->object = $object ? $object->getForm()->getObject() : false;
            }
        }

        return $this->object ?: null;
    }

    /**
     * @param string|null $type
     * @return FlexDirectoryInterface|null
     */
    public function getDirectory(string $type = null): ?FlexDirectoryInterface
    {
        $type = $type ?? $this->target;

        if ($type && null === $this->directory) {
            $this->directory = Grav::instance()['flex_objects']->getDirectory($type);
        }

        return $this->directory;
    }

    /**
     * @return FlexCollectionInterface|null
     */
    public function getCollection(): ?FlexCollectionInterface
    {
        if (null === $this->collection) {
            $directory = $this->getDirectory();

            $this->collection = $directory ? $directory->getCollection() : null;
        }

        return $this->collection;
    }

    /**
     * @param string $msg
     * @param string $type
     * @return void
     */
    public function setMessage(string $msg, string $type = 'info'): void
    {
        /** @var Message $messages */
        $messages = $this->grav['messages'];
        $messages->add($msg, $type);
    }

    /**
     * @return bool
     */
    public function isActive(): bool
    {
        return (bool) $this->active;
    }

    /**
     * @param string $location
     * @return void
     */
    public function setLocation(string $location): void
    {
        $this->location = $location;
    }

    /**
     * @return string|null
     */
    public function getLocation(): ?string
    {
        return $this->location;
    }

    /**
     * @param string $action
     * @return void
     */
    public function setAction(string $action): void
    {
        $this->action = $action;
    }

    /**
     * @return string|null
     */
    public function getAction(): ?string
    {
        return $this->action;
    }

    /**
     * @param string $task
     * @return void
     */
    public function setTask(string $task): void
    {
        $this->task = $task;
    }

    /**
     * @return string|null
     */
    public function getTask(): ?string
    {
        return $this->task;
    }

    /**
     * @param string $target
     * @return void
     */
    public function setTarget(string $target): void
    {
        $this->target = $target;
    }

    /**
     * @return string|null
     */
    public function getTarget(): ?string
    {
        return $this->target;
    }

    /**
     * @param string $id
     * @return void
     */
    public function setId(string $id): void
    {
        $this->id = $id;
    }

    /**
     * @return string|null
     */
    public function getId(): ?string
    {
        return $this->id;
    }

    /**
     * Sets the page redirect.
     *
     * @param string $path The path to redirect to
     * @param int    $code The HTTP redirect code
     * @return void
     */
    public function setRedirect(string $path, int $code = 303): void
    {
        $this->redirect     = $path;
        $this->redirectCode = (int)$code;
    }

    /**
     * Redirect to the route stored in $this->redirect
     *
     * @return void
     */
    public function redirect(): void
    {
        $this->admin->redirect($this->redirect, $this->redirectCode);
    }

    /**
     * @return array
     */
    public function getAdminRoutes(): array
    {
        if (null === $this->adminRoutes) {
            $routes = [];
            /** @var FlexDirectoryInterface $directory */
            foreach ($this->getFlex()->getDirectories() as $directory) {
                $config = $directory->getConfig('admin');
                if (!$directory->isEnabled() || !empty($config['disabled'])) {
                    continue;
                }

                // Alias under flex-objects (always exists, but can be redirected).
                $routes["/flex-objects/{$directory->getFlexType()}/"] = ['directory' => $directory];

                $route = $config['router']['path'] ?? $config['menu']['list']['route'] ?? null;
                if ($route) {
                    $routes[$route . '/'] = ['directory' => $directory];
                }

                $redirects = (array)($config['router']['redirects'] ?? null);
                foreach ($redirects as $redirectFrom => $redirectTo) {
                    $redirectFrom .= '/';
                    if (!isset($routes[$redirectFrom])) {
                        $routes[$redirectFrom] = ['directory' => $directory, 'redirect' => $redirectTo];
                    }
                }

                $actions = (array)($config['router']['actions'] ?? null);
                foreach ($actions as $name => $action) {
                    if (is_array($action)) {
                        $path = $action['path'] ?? null;
                    } else {
                        $path = $action;
                    }
                    if ($path !== null) {
                        $routes[$path . '/'] = ['directory' => $directory, 'action' => $name];
                    }
                }
            }

            $this->adminRoutes = $routes;
        }

        return $this->adminRoutes;
    }

    /**
     * Return true if multilang is active
     *
     * @return bool True if multilang is active
     */
    protected function isMultilang(): bool
    {
        /** @var Language $language */
        $language = $this->grav['language'];

        return $language->enabled();
    }

    protected function validateNonce(): bool
    {
        $nonce_action = $this->nonce_action;
        $nonce = $this->post[$this->nonce_name] ?? $this->grav['uri']->param($this->nonce_name) ?? $this->grav['uri']->query($this->nonce_name);

        if (!$nonce) {
            $nonce = $this->post['admin-nonce'] ?? $this->grav['uri']->param('admin-nonce') ?? $this->grav['uri']->query('admin-nonce');
            $nonce_action = 'admin-form';
        }

        return $nonce && Utils::verifyNonce($nonce, $nonce_action);
    }

    /**
     * Prepare and return POST data.
     *
     * @param array $post
     * @return array
     */
    protected function getPost(array $post): array
    {
        unset($post['task']);

        // Decode JSON encoded fields and merge them to data.
        if (isset($post['_json'])) {
            $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
            unset($post['_json']);
        }

        $post = $this->cleanDataKeys($post);

        return $post;
    }

    /**
     * @param ResponseInterface $response
     * @return never-return
     */
    protected function close(ResponseInterface $response): void
    {
        $this->grav->close($response);
    }

    /**
     * Recursively JSON decode data.
     *
     * @param  array $data
     * @return array
     */
    protected function jsonDecode(array $data)
    {
        foreach ($data as &$value) {
            if (is_array($value)) {
                $value = $this->jsonDecode($value);
            } else {
                $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
            }
        }

        return $data;
    }

    /**
     * @param array $source
     * @return array
     */
    protected function cleanDataKeys($source = []): array
    {
        $out = [];

        if (is_array($source)) {
            foreach ($source as $key => $value) {
                $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
                if (is_array($value)) {
                    $out[$key] = $this->cleanDataKeys($value);
                } else {
                    $out[$key] = $value;
                }
            }
        }

        return $out;
    }

    /**
     * @return string
     */
    public function getLanguage(): string
    {
        return $this->admin->language ?? '';
    }

    /**
     * @return Config
     */
    protected function getConfig(): Config
    {
        return $this->grav['config'];
    }

    /**
     * @return ServerRequestInterface
     */
    protected function getRequest(): ServerRequestInterface
    {
        return $this->grav['request'];
    }
}