<?php

namespace apexl\Io\modules\menu\entities;

use apexl\entityCore\enums\Casts;
use apexl\entityCore\traits\HasCasts;
use apexl\Io\exceptions\RecordNotFoundException;
use apexl\Io\includes\Entity;
use apexl\Io\modules\menu\entities\operators\menuOperator;
use apexl\Io\modules\menu\Factories\MenuItemFactory;
use apexl\Io\modules\menu\includes\menuItem;
use apexl\Io\modules\user\entities\userEntity;
use apexl\Io\modules\user\enums\permissions\Common;
use apexl\Io\modules\user\exceptions\PermissionException;
use apexl\Io\services\HttpPaths;
use apexl\Utils\Arrays\Merge;
use DateTimeImmutable;
use function apexl\Io\container;

/**
 * @property int $id
 * @property string $name
 * @property string $module
 * @property string $permission
 * @property bool $active
 * @property int $weight
 * @property DateTimeImmutable $created
 * @property DateTimeImmutable $created_by
 * @property DateTimeImmutable $modified
 * @property DateTimeImmutable $modified_by
 *
 * @mixin menuOperator
 *
 * @Todo Decouple from Database
 * @Todo Make play nice with DI
 */
class menuEntity extends Entity
{
    use HasCasts;

    public final const string TABLE = 'menus';

    /**
     * @var menuItem[]
     */
    protected array $menuItems = [];

    protected array $links = [];
    private HttpPaths $httpPaths;
    private MenuItemFactory $menuItemFactory;

    public function __construct()
    {
        $this->httpPaths = container()->get(HttpPaths::class);
        $this->menuItemFactory = container()->get(MenuItemFactory::class);
        parent::__construct(self::TABLE);
        $this->setOperator(new menuOperator(self::TABLE));
        //set Defaults
        $this->name = 'navigation';
        $this->module = 'menu';
        $this->permission = Common::ALLOW_ALL;
        $this->active = 1;
    }

    public function casts(): array
    {
        return [
            'permission' => Casts::SERIALIZE,
        ];
    }

    /**
     * @throws RecordNotFoundException
     */
    public function loadByName($name, $skipAccessCheck = false): void
    {
        $this->loadBy('name', $name, $skipAccessCheck);

        $this->buildMenu();
    }

    /**
     * @todo This needs to be cached.
     */
    protected function buildMenu(): void
    {
        $menuItems = [];
        if (!empty($this->data['links'])) {
            foreach ($this->data['links'] as $item) {
                try {
                    $menuItem = $this->menuItemFactory->make($item);
                } catch (PermissionException) {
                    continue;
                }
                $menuItem = $this->hookManager->processHook('menuItemAlter', $menuItem);

                //do we have child links? Is so, loop and load.
                if ($item['links'] ?? false) {
                    $subLinks = [];
                    foreach ($item['links'] as $subItem) {
                        try {
                            $subMenuItem = $this->menuItemFactory->make($subItem);
                        } catch (PermissionException) {
                            continue;
                        }
                        $subMenuItem = $this->hookManager->processHook('menuItemAlter', $subMenuItem);
                        //weighting, treat 0 as 'stack' so we dont do this on every pass..
                        if ($menuItem->weight !== 0) {
                            Merge::arrayByWeight(
                                $subMenuItem->weight,
                                (object) get_object_vars($subMenuItem),
                                $subLinks
                            );
                        } else {
                            $subLinks[] = (object) get_object_vars($subMenuItem);
                        }
                    }
                    $menuItem->links = $subLinks;
                }
                //test if this is an empty parent. If so, and the parent doesn't link, omit it completely.
                //parent weighting, treat 0 as 'stack' so we dont do this on every pass..
                if ($menuItem->weight !== 0) {
                    Merge::arrayByWeight($menuItem->weight, (object) get_object_vars($menuItem), $menuItems);
                } else {
                    $menuItems[] = (object) get_object_vars($menuItem);
                }
            }
        }

        foreach ($menuItems as $menuItem) {
            $this->menuItems[$menuItem->href] = $menuItem;
        }
    }

    /**
     * @param $routeName
     * @param string $title
     * @param string $icon
     * @param int $weight
     * @param bool $parentRoute
     * @return $this
     */
    public function addToMenu(
        $routeName,
        $title = '',
        $icon = '',
        $weight = 0,
        $parentRoute = false,
        $replacements = []
    ): static {
        $link = [
            'routeName' => $routeName,
            'title' => $title,
            'icon' => $icon,
            'weight' => $weight,
            'replacements' => $replacements,
        ];

        // Synthetic key: a route registered multiple times with different
        // replacements (e.g. per-stage admin links) needs a distinct storage
        // key for each variation. Re-registrations without replacements
        // still collide on routeName alone — preserving the "re-register
        // to override" behaviour callers rely on.
        $key = $routeName . (empty($replacements) ? '' : '#' . md5(serialize($replacements)));

        if ($parentRoute) {
            if (!isset($this->data['links'][$parentRoute])) {
                //set some defaults, just incase.
                $this->data['links'][$parentRoute] = [
                    'routeName' => $parentRoute,
                    'title' => $parentRoute,
                ];
            }
            $this->data['links'][$parentRoute]['links'][$key] = $link;
        } elseif (isset($this->data['links'][$key]['links'])) {
            //assume we're overriding settings, keep child links if this is already set.
            $childLinks = $this->data['links'][$key]['links'];
            $this->data['links'][$key] = $link;
            $this->data['links'][$key]['links'] = $childLinks;
        } else {
            $this->data['links'][$key] = $link;
        }

        return $this;
    }

    /**
     * Build the entity into a menu structure for display.
     * @param bool $filterByPermission
     * @todo perhaps move this into something different like a display module?
     */
    public function getAllItemsAsMenu(userEntity $user, bool $filterByPermission = true): array
    {
        $this->buildMenu();
        $menu = [];
        foreach ($this->menuItems as $menuGroup) {
            if ($filterByPermission) {
                $item = $this->checkAccess($menuGroup, $user);
                if (!empty((array) $item)) { //typecast in case of empty object.
                    $menu[] = $item;
                }
            }
        }

        return $menu;
    }

    protected function checkAccess($menuItem, userEntity $user): \apexl\Io\modules\menu\includes\menuItem|\stdClass
    {
        $allowed = (object) [];
        if (isset($menuItem->href)) {
            if ($user->isAllowed($menuItem->permission)) {
                $allowed = clone $menuItem;
                if (isset($menuItem->links)) {
                    //clear the links from the allowed var since we want to check access.
                    unset($allowed->links);
                    foreach ($menuItem->links as $link) {
                        if (!empty((array) $this->checkAccess($link, $user))) {
                            $allowed->links[] = $link;
                        }
                    }
                }
            }
        } else {
            $allowed = $menuItem;
        }

        return $allowed;
    }

    /**
     * Generate breadcrumbs for the provided path.
     * @param $route
     */
    public function generateBreadcrumbs($route): array
    {
        //get the parent item and return the stack (usually needed for breadcrumbs)
        $currentRouteName = $this->httpPaths->getCurrentRouteName();
        $currentPath = $this->httpPaths->getPath();
        //echo $currentRouteName . ' ||| ' . $currentPath . "\n\n";
        $this->buildMenu();
        $parts = explode('/', ltrim((string) $currentPath, '/'));
        //we want to check each component for a matching route. If the component contains a dynamic key, we need to check for callbacks
        $reconstructed = '';
        $stack = [];

        foreach ($parts as $part) {
            $reconstructed = $reconstructed . '/' . $part;
            if (!empty($this->menuItems)) {
                foreach ($this->menuItems as $item) {
                    //  echo "\n".$item->href . ' --- ' . $reconstructed;
                    // print_r($item);
                    if ($item->href == $reconstructed) {
                        $stack[] = $item;
                    } elseif (isset($item->links)) {
                        foreach ($item->links as $childItem) {
                            if ($childItem->href == $reconstructed) {
                                $parentItem = clone $item;
                                unset($parentItem->href);
                                $stack[] = $parentItem;
                                $stack[] = $childItem;
                            }
                        }
                    }
                }
            }
        }

        return $stack;
    }

    /**
     * @param $routeName
     * @return false|mixed
     */
    public function getMenuItem($routeName): mixed
    {
        //strip any params
        if (($params = strpos((string) $routeName, '?')) !== false) { //Bool safe check
            [$routeName] = explode('?', (string) $routeName);
        }
        if (empty($routeName)) {
            $routeName = '/';
        }

        //check for callbacks and override if available.
        return $this->links[$routeName] ?? $this->checkSublinks($routeName) ?? null;
    }

    /**
     * @param $routeName
     * @return false|mixed
     */
    protected function checkSublinks($routeName): mixed
    {
        foreach ($this->links as $item) {
            if (!$links = $item->links ?? false) {
                continue;
            }
            foreach ($item->links as $subLink) {
                if ($subLink->routeName == $routeName) {
                    return $subLink;
                }
            }
        }

        return null;
    }

    public function isCurrentMenu(): bool
    {
        $currentRouteName = $this->httpPaths->getCurrentRouteName();

        if (!empty($this->menuItems)) {
            foreach ($this->menuItems as $item) {
                if ($item->name == $currentRouteName) {
                    return true;
                }
            }
        }

        return false;
    }
}
