<?php

/**
 * @package Dotclear
 * @subpackage Backend
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright AGPL-3.0
 */
declare(strict_types=1);

namespace Dotclear\Core\Backend;

use ArrayObject;
use Dotclear\App;
use Dotclear\Interface\Core\UserWorkspaceInterface;

/**
 * Favorites handling facilities
 *
 * @phpstan-type TFavoriteData array{
 *     title?: ?string,
 *     url?: ?string,
 *     small-icon?: null|string|string[],
 *     large-icon?: null|string|string[],
 *     permissions?: null|string|bool,
 *     dashboard_cb?: ?callable,
 *     active_cb?: ?callable,
 *     active?: bool,
 *     menu-icon?: ?Icon,
 *     dashboard-icon?: ?Icon
 * }
 */
class Favorites
{
    /**
     * List of favorite definitions
     *
     * @var array<string, Favorite>  $favorites
     */
    protected array $favorites = [];

    /**
     * Current favorite landing workspace
     */
    protected UserWorkspaceInterface $workspace;

    /**
     * List of user-defined favorite ids
     *
     * @var string[]   $local_favorites_ids
     */
    protected array $local_favorites_ids;

    /**
     * List of globally-defined favorite ids
     *
     * @var string[]   $global_favorites_ids
     */
    protected array $global_favorites_ids;

    /**
     * List of user preferences (made from either one of the 2 above, or not!)
     *
     * @var array<string, Favorite>    $user_favorites
     */
    protected $user_favorites = [];

    /**
     * Class constructor
     */
    public function __construct()
    {
        $this->workspace = App::auth()->prefs()->get('dashboard');

        if ($this->workspace->prefExists('favorites')) {
            /**
             * @var ?string[] $local_favs
             */
            $local_favs = $this->workspace->getLocal('favorites');

            /**
             * @var ?string[] $global_favs
             */
            $global_favs = $this->workspace->getGlobal('favorites');

            // Since we never know what user puts through user:preferences ...
            $this->local_favorites_ids  = is_array($local_favs) ? $local_favs : [];
            $this->global_favorites_ids = is_array($global_favs) ? $global_favs : [];
        } else {
            // No favorite defined ? Huhu, let's go for a migration
            $this->migrateFavorites();
        }
    }

    /**
     * Sets up favorites, fetch user favorites (against his permissions)
     * This method is to be called after loading plugins
     */
    public function setup(): void
    {
        $this->initDefaultFavorites();
        $this->legacyFavorites();
        # --BEHAVIOR-- adminDashboardFavoritesV2 -- Favorites
        App::behavior()->callBehavior('adminDashboardFavoritesV2', $this);
        $this->setUserPrefs();
    }

    /**
     * Retrieves a favorite (complete description) from its id.
     *
     * @param string  $id   the favorite id
     *
     * @return Favorite|false   Some of the favorite properties, false if not found (or not permitted)
     */
    public function getFavorite(string $id): false|Favorite
    {
        if (!isset($this->favorites[$id])) {
            return false;
        }

        $favorite = $this->favorites[$id];
        if (!is_null($favorite->permissions())) {
            if (is_bool($favorite->permissions())) {
                if (!$favorite->permissions()) {
                    return false;
                }
            } elseif (!App::auth()->check($favorite->permissions(), App::blog()->id())) {
                return false;
            }
        } elseif (!App::auth()->isSuperAdmin()) {
            return false;
        }

        return $favorite;
    }

    /**
     * getFavorites - retrieves a list of favorites.
     *
     * @param  array<string>  $ids   an array of ids, as defined in getFavorite.
     *
     * @return array<string, Favorite> array of favorites, can be empty if ids are not found (or not permitted)
     */
    public function getFavorites(array $ids): array
    {
        $favorites = [];
        foreach ($ids as $id) {
            $favorite = $this->getFavorite($id);
            if ($favorite !== false) {
                $favorites[$id] = $favorite;
            }
        }

        return $favorites;
    }

    /**
     * Get user favorites from settings.
     *
     * These are complete favorites, not ids only returned favorites are the first non-empty list from :
     * - user-defined favorites
     * - globally-defined favorites
     * - a failback list "new post" (shall never be empty)
     *
     * This method is called by ::setup()
     */
    protected function setUserPrefs(): void
    {
        $this->user_favorites = $this->getFavorites($this->local_favorites_ids);
        if ($this->user_favorites === []) {
            $this->user_favorites = $this->getFavorites($this->global_favorites_ids);
        }

        if ($this->user_favorites === []) {
            $this->user_favorites = $this->getFavorites(['new_post']);
        }

        $request_uri = isset($_SERVER['REQUEST_URI']) && is_string($request_uri = $_SERVER['REQUEST_URI']) ? $request_uri : '';
        $uri         = explode('?', $request_uri);

        // take only last part of the URI, all plugins work like that
        $uri[0] = preg_replace('#(.*?)([^/]+)$#', '$2', $uri[0]);

        // Loop over prefs to enable active favorites
        foreach ($this->user_favorites as $favorite) {
            // duplicate request URI on each loop as it takes previous pref value ?!
            $url = $uri;
            if ($favorite->activeCallback()) {
                // Use callback if defined to match whether favorite is active or not
                $favorite->setActive($favorite->callActiveCallback((string) $url[0], $_REQUEST));
            } else {
                // Failback active detection. We test against URI name & parameters
                $favorite->setActive(true); // true until something proves it is false
                $url = explode('?', (string) $favorite->url(), 2);
                if (!preg_match('/' . preg_quote($url[0], '/') . '/', $request_uri)) {
                    $favorite->setActive(false); // no URI match
                }

                if (count($url) === 2) {
                    parse_str(html_entity_decode($url[1]), $result);
                    // test against each request parameter.
                    foreach ($result as $key => $value) {
                        if (!isset($_REQUEST[$key]) || $_REQUEST[$key] !== $value) {
                            $favorite->setActive(false);
                        }
                    }
                }
            }
        }
    }

    /**
     * migrateFavorites - migrate dc < 2.6 favorites to new format
     */
    protected function migrateFavorites(): void
    {
        $favorites_workspace = App::auth()->prefs()->get('favorites');

        $this->local_favorites_ids  = [];
        $this->global_favorites_ids = [];

        foreach ($favorites_workspace->dumpPrefs() as $pref) {
            $value = is_string($value = $pref['value']) ? $value : '';
            if ($value !== '') {
                $favorite = @unserialize($value);
                if (is_array($favorite) && isset($favorite['name']) && is_string($favorite['name'])) {
                    if ($pref['global']) {
                        $this->global_favorites_ids[] = $favorite['name'];
                    } else {
                        $this->local_favorites_ids[] = $favorite['name'];
                    }
                }
            }
        }

        $this->workspace->put('favorites', $this->global_favorites_ids, App::userWorkspace()::WS_ARRAY, 'User favorites', true, true);
        $this->workspace->put('favorites', $this->local_favorites_ids, App::userWorkspace()::WS_ARRAY);

        $this->user_favorites = $this->getFavorites($this->local_favorites_ids);
    }

    /**
     * legacyFavorites - handle legacy favorites using adminDashboardFavs behavior
     */
    protected function legacyFavorites(): void
    {
        $favorites = new ArrayObject();

        # --BEHAVIOR-- adminDashboardFavsV2 -- ArrayObject
        App::behavior()->callBehavior('adminDashboardFavsV2', $favorites);

        /**
         * @var array{array-key, mixed}
         * @phpstan-ignore varTag.type
         */
        $favs = $favorites->getArrayCopy();

        foreach ($favs as $fav) {
            if (is_array($fav)) {
                $id = isset($fav[0]) && is_string($id = $fav[0]) ? $id : '';
                if ($id !== '') {
                    $title          = isset($fav[1]) && is_string($title = $fav[1]) ? $title : '';
                    $url            = isset($fav[2]) && is_string($url = $fav[2]) ? $url : '';
                    $small_icon     = isset($fav[3]) && is_string($small_icon = $fav[3]) ? $small_icon : '';
                    $large_icon     = isset($fav[4]) && is_string($large_icon = $fav[4]) ? $large_icon : '';
                    $permissions    = isset($fav[5]) && is_string($permissions = $fav[5]) ? $permissions : '';
                    $menu_icon      = isset($fav[6]) && $fav[6] instanceof Icon ? $fav[6] : null;
                    $dashboard_icon = isset($fav[7]) && $fav[7] instanceof Icon ? $fav[7] : null;

                    $this->register($id, [
                        'title'          => __($title),
                        'url'            => $url,
                        'small-icon'     => $small_icon,
                        'large-icon'     => $large_icon,
                        'permissions'    => $permissions,
                        'dashboard_cb'   => null,
                        'active_cb'      => null,
                        'active'         => false,
                        'menu-icon'      => $menu_icon,
                        'dashboard-icon' => $dashboard_icon,
                    ]);
                }
            }
        }
    }

    /**
     * Returns favorites that correspond to current user
     * (may be local, global, or failback favorites)
     *
     * @return array<string, Favorite> array of favorites
     */
    public function getUserFavorites(): array
    {
        return $this->user_favorites;
    }

    /**
     * Returns user-defined or global favorites ids list
     * shall not be called outside preferences.php...
     *
     * @param boolean  $global   if true, retrieve global favs, user favs otherwise
     *
     * @return string[] array of favorites ids (only ids, not enriched)
     */
    public function getFavoriteIDs(bool $global = false): array
    {
        return $global ? $this->global_favorites_ids : $this->local_favorites_ids;
    }

    /**
     * Stores user-defined or global favorites ids list
     * shall not be called outside preferences.php...
     *
     * @param string[]          $ids     list of fav ids
     * @param boolean           $global  if true, retrieve global favs, user favs otherwise
     */
    public function setFavoriteIDs(array $ids, bool $global = false): void
    {
        $this->workspace->put('favorites', $ids, App::userWorkspace()::WS_ARRAY, null, true, $global);
    }

    /**
     * Returns all available fav ids
     *
     * @return string[] array of favorites ids (only ids, not enriched)
     */
    public function getAvailableFavoritesIDs(): array
    {
        return array_keys($this->favorites);
    }

    /**
     * Adds favorites section title to sidebar menu
     * shall not be called outside backend Utility...
     *
     * @param   Menus   $menu   admin menu
     */
    public function appendMenuSection(Menus $menu): void
    {
        $menu->prependSection(Menus::MENU_FAVORITES, new Menu('favorites-menu', __('My favorites')));
    }

    /**
     * Adds favorites items title to sidebar menu
     * shall not be called outside backend Utility...
     *
     * @param Menus  $menu   admin menu
     */
    public function appendMenu(Menus $menu): void
    {
        foreach ($this->user_favorites as $favorite_id => $favorite) {
            $menu[Menus::MENU_FAVORITES]?->addItem(
                $favorite->title()    ?? '',
                $favorite->url()      ?? '',
                $favorite->menuIcon() ?? $favorite->smallIcon() ?? '',
                $favorite->active(),
                true,
                $favorite_id . '-fav',
                null,
                true
            );
        }
    }

    /**
     * Adds favorites icons to index page
     * shall not be called outside Home.php...
     *
     * @param ArrayObject<string, ArrayObject<int, mixed> >  $icons   dashboard icon list to enrich
     */
    public function appendDashboardIcons(ArrayObject $icons): void
    {
        foreach ($this->user_favorites as $favorite_id => $favorite) {
            $favorite->callDashboardCallback();
            /*
             * $icons items structure:
             * [0] = title
             * [1] = url
             * [2] = icons (usually array (light/dark))
             * [3] = additional informations (usually set by 3rd party plugins)
             * [4] = icons (as Icon or null)
             */
            $icons[$favorite_id] = new ArrayObject([
                $favorite->title()     ?? '',
                $favorite->url()       ?? '',
                $favorite->largeIcon() ?? '',
                null,
                $favorite->dashboardIcon(),
            ]);
            # --BEHAVIOR-- adminDashboardFavsIconV2 -- string, ArrayObject
            App::behavior()->callBehavior('adminDashboardFavsIconV2', $favorite_id, $icons[$favorite_id]);
        }
    }

    /**
     * Registers a new favorite definition
     *
     * @param string                $id   favorite id
     * @param TFavoriteData         $data favorite information
     *
     * @return Favorites instance
     */
    public function register(string $id, array $data): Favorites
    {
        $small_icon = null;
        if (isset($data['small-icon'])) {
            $small_icon = [];
            if (is_array($data['small-icon'])) {
                if (isset($data['small-icon'][0])) {
                    $small_icon[0] = $data['small-icon'][0];
                    if (isset($data['small-icon'][1])) {
                        $small_icon[1] = $data['small-icon'][1];
                    }
                }

                if ($small_icon === []) {
                    $small_icon = null;
                }
            } else {
                $small_icon = $data['small-icon'];
            }
        }

        $menu_icon = $data['menu-icon'] ?? null;
        if ($menu_icon === null) {
            if ($small_icon === null) {
                $menu_icon = null;
            } elseif (is_string($small_icon)) {
                $menu_icon = new Icon($small_icon);
            } else {
                $menu_icon = new Icon($small_icon[0], $small_icon[1] ?? '');
            }
        }

        $large_icon = null;
        if (isset($data['large-icon'])) {
            $large_icon = [];
            if (is_array($data['large-icon'])) {
                if (isset($data['large-icon'][0])) {
                    $large_icon[0] = $data['large-icon'][0];
                    if (isset($data['large-icon'][1])) {
                        $large_icon[1] = $data['large-icon'][1];
                    }
                }

                if ($large_icon === []) {
                    $large_icon = null;
                }
            } else {
                $large_icon = $data['large-icon'];
            }
        }

        $dashboard_icon = $data['dashboard-icon'] ?? null;
        if ($dashboard_icon === null) {
            if ($large_icon === null) {
                $dashboard_icon = null;
            } elseif (is_string($large_icon)) {
                $dashboard_icon = new Icon($large_icon);
            } else {
                $dashboard_icon = new Icon($large_icon[0], $large_icon[1] ?? '');
            }
        }

        $this->favorites[$id] = new Favorite(
            $id,
            $data['title'] ?? null,
            $data['url']   ?? null,
            $small_icon,
            $large_icon,
            $data['permissions']  ?? null,
            $data['dashboard_cb'] ?? null,
            $data['active_cb']    ?? null,
            $data['active']       ?? false,
            $menu_icon,
            $dashboard_icon
        );

        return $this;
    }

    /**
     * Registers a list of favorites definition
     *
     * @param array<string, TFavoriteData> $data Array of favorites to register.
     *
     * @return Favorites instance
     */
    public function registerMultiple(array $data): Favorites
    {
        foreach ($data as $favorite_id => $favorite_data) {
            $this->register($favorite_id, $favorite_data);
        }

        return $this;
    }

    /**
     * exists - tells whether a fav definition exists or not
     *
     * @param string $id : the fav id to test
     *
     * @return bool true if the fav definition exists, false otherwise
     */
    public function exists(string $id): bool
    {
        return isset($this->favorites[$id]);
    }

    /**
     * Initializes the default favorites.
     */
    public function initDefaultFavorites(): void
    {
        $this->registerMultiple([
            'prefs' => [
                'title'          => __('Preferences'),
                'url'            => App::backend()->url()->get('admin.user.preferences'),
                'menu-icon'      => new Icon('images/menu/user-pref.svg'),
                'dashboard-icon' => new Icon('images/menu/user-pref.svg'),
            ],

            'new_post' => [
                'title'       => __('New post'),
                'url'         => App::backend()->url()->get('admin.post'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_USAGE,
                    App::auth()::PERMISSION_CONTENT_ADMIN,
                ]),
                'active_cb'      => fn (string $request_uri, array $request_params): bool => App::backend()->url()::INDEX === $request_uri && isset($request_params['process']) && $request_params['process'] === 'Post' && !isset($request_params['id']),
                'menu-icon'      => new Icon('images/menu/edit.svg', 'images/menu/edit-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/edit.svg', 'images/menu/edit-dark.svg'),
            ],

            'posts' => [
                'title'       => __('Posts'),
                'url'         => App::backend()->url()->get('admin.posts'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_USAGE,
                    App::auth()::PERMISSION_CONTENT_ADMIN,
                ]),
                'dashboard_cb' => function (ArrayObject $icon): void {
                    $post_count    = App::blog()->getPosts([], true)->cardinal();
                    $str_entries   = __('%d post', '%d posts', $post_count);
                    $icon['title'] = sprintf($str_entries, $post_count);
                },
                'menu-icon'      => new Icon('images/menu/entries.svg', 'images/menu/entries-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/entries.svg', 'images/menu/entries-dark.svg'),
            ],

            'comments' => [
                'title'       => __('Comments'),
                'url'         => App::backend()->url()->get('admin.comments'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_USAGE,
                    App::auth()::PERMISSION_CONTENT_ADMIN,
                ]),
                'dashboard_cb' => function (ArrayObject $icon): void {
                    $comment_count = App::blog()->getComments([], true)->cardinal();
                    $str_comments  = __('%d comment', '%d comments', $comment_count);
                    $icon['title'] = sprintf($str_comments, $comment_count);
                },
                'menu-icon'      => new Icon('images/menu/comments.svg', 'images/menu/comments-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/comments.svg', 'images/menu/comments-dark.svg'),
            ],

            'search' => [
                'title'       => __('Search'),
                'url'         => App::backend()->url()->get('admin.search'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_USAGE,
                    App::auth()::PERMISSION_CONTENT_ADMIN,
                ]),
                'menu-icon'      => new Icon('images/menu/search.svg', 'images/menu/search-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/search.svg', 'images/menu/search-dark.svg'),
            ],

            'categories' => [
                'title'       => __('Categories'),
                'url'         => App::backend()->url()->get('admin.categories'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_CATEGORIES,
                ]),
                'menu-icon'      => new Icon('images/menu/categories.svg', 'images/menu/categories-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/categories.svg', 'images/menu/categories-dark.svg'),
            ],

            'media' => [
                'title'       => __('Media manager'),
                'url'         => App::backend()->url()->get('admin.media'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_MEDIA,
                    App::auth()::PERMISSION_MEDIA_ADMIN,
                ]),
                'menu-icon'      => new Icon('images/menu/media.svg', 'images/menu/media-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/media.svg', 'images/menu/media-dark.svg'),
            ],

            'blog_pref' => [
                'title'       => __('Blog settings'),
                'url'         => App::backend()->url()->get('admin.blog.pref'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_ADMIN,
                ]),
                'menu-icon'      => new Icon('images/menu/blog-pref.svg', 'images/menu/blog-pref-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/blog-pref.svg', 'images/menu/blog-pref-dark.svg'),
            ],

            'blog_theme' => [
                'title'       => __('Blog appearance'),
                'url'         => App::backend()->url()->get('admin.blog.theme'),
                'permissions' => App::auth()->makePermissions([
                    App::auth()::PERMISSION_ADMIN,
                ]),
                'menu-icon'      => new Icon('images/menu/themes.svg', 'images/menu/themes-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/themes.svg', 'images/menu/themes-dark.svg'),
            ],

            'blogs' => [
                'title'       => __('Blogs'),
                'url'         => App::backend()->url()->get('admin.blogs'),
                'permissions' => !App::auth()->isSuperAdmin() && App::auth()->getBlogCount() > 1 ? App::auth()->makePermissions([
                    App::auth()::PERMISSION_USAGE,
                    App::auth()::PERMISSION_CONTENT_ADMIN,
                ]) : null,
                'menu-icon'      => new Icon('images/menu/blogs.svg', 'images/menu/blogs-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/blogs.svg', 'images/menu/blogs-dark.svg'),
            ],

            'users' => [
                'title'          => __('Users'),
                'url'            => App::backend()->url()->get('admin.users'),
                'menu-icon'      => new Icon('images/menu/users.svg'),
                'dashboard-icon' => new Icon('images/menu/users.svg'),
            ],

            'plugins' => [
                'title'          => __('Plugins management'),
                'url'            => App::backend()->url()->get('admin.plugins'),
                'menu-icon'      => new Icon('images/menu/plugins.svg', 'images/menu/plugins-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/plugins.svg', 'images/menu/plugins-dark.svg'),
            ],

            'settings' => [
                'title'          => __('Plugins settings'),
                'url'            => App::backend()->url()->get('admin.settings'),
                'menu-icon'      => new Icon('images/menu/settings.svg', 'images/menu/settings-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/settings.svg', 'images/menu/settings-dark.svg'),
            ],

            'langs' => [
                'title'          => __('Languages'),
                'url'            => App::backend()->url()->get('admin.langs'),
                'menu-icon'      => new Icon('images/menu/langs.svg', 'images/menu/langs-dark.svg'),
                'dashboard-icon' => new Icon('images/menu/langs.svg', 'images/menu/langs-dark.svg'),
            ],

            'help' => [
                'title'          => __('Global help'),
                'url'            => App::backend()->url()->get('admin.help'),
                'menu-icon'      => new Icon('images/menu/help.svg'),
                'dashboard-icon' => new Icon('images/menu/help.svg'),
            ],
        ]);
    }
}
