<?php

/**
 * Site Menu Class
 *
 * This code is used to make up Site Menu plugin
 *
 * @category Plugins
 * @package  Mtc_Site_Menu
 * @author   mtc. Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 * @author   mtc. Peter Carlyle <peter.carlyle@mtcmedia.co.uk>
 * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
 */

namespace Mtc\Plugins\SiteMenu\Classes;

use CategoryHelper;
use CMSNav;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Mtc\Shop\Brand;
use Mtc\Shop\Category;
use MtcPharmacy\Multisite\Classes\MultisiteManager;
use Util;

/**
 * Site Menu plugin Class.
 * Creates and manipulates with menu items.
 * Extends Eloquent model
 *
 * @author   mtc. Martins Fridenbergs <martins.fridenbergs@mtcmedia.co.uk>
 *
 * @version  2017-04-18
 */
class SiteMenu extends Model
{
    use SoftDeletes;

    /**
     * @var String Classes associated table
     */
    protected $table = 'site_menu';

    /**
     * @var Array List of fillable variables
     */
    protected $fillable = [
        'name',
        'url',
        'target',
        'parent_id',
        'category_list',
        'brand_list',
        'site_tree',
        'css_class',
        'published',
        'category_id',
        'single_category',
        'single_cms_page',
    ];

    public static function boot(): void
    {
        parent::boot();

        self::deleting(function (self $menu_entry): void {
            $menu_entry->children
                ->each(function ($menu_entry): void {
                    $menu_entry->delete();
                });

            self::invalidateAllMenuCaches();
        });

        self::saved(function ($menu_entry): void {
            // When a menu is saved, we need to invalidate:
            // 1. All top-level menu caches (in case structure changed)
            // 2. The specific menu's cache
            // 3. Parent menu's cache (if this is a sub-menu)
            // 4. All ancestor caches up the tree

            self::invalidateMenuAndAncestors($menu_entry);
        });
    }

    /**
     * @var boolean Disables use of eloquent timestamps
     */
    public $timestamps = false;

    /**
     * Generate cache key for a menu
     *
     * @param string $menuName
     * @param string $variant Optional variant identifier (e.g., '_without_children')
     * @return string
     */
    private static function getCacheKey(string $menuName, string $variant = ''): string
    {
        $normalizedMenu = Str::slug($menuName, '_');
        $site = defined('SITE') ? SITE : 'default';
        $db = defined('MYSQL_DB') ? MYSQL_DB : 'default';

        return "site_menu:{$site}:{$db}:{$normalizedMenu}{$variant}";
    }

    /**
     * Fetch menu tree with caching
     *
     * @param SiteMenu $menu
     * @return Collection
     */
    public static function fetchTree($menu): Collection
    {
        $cacheKey = self::getCacheKey($menu->name.'_tree');
        $ttl = now()->addHours(6);

        return Cache::remember($cacheKey, $ttl, function () use ($menu) {
            return $menu->getSiteMenuTree(false);
        });
    }

    /**
     * Render menu HTML
     *
     * @param string $name
     * @param Collection $menu
     * @param array $options
     * @return string
     */
    private static function getMenu(string $name, Collection $menu, array $options): string
    {
        $html = App::make('twig')->render('SiteMenu/menu.twig', [
            'items' => $menu,
            'options' => $options,
            'top_level' => true,
        ]);

        return $html;
    }

    /**
     * SiteMenu::renderMenu()
     *
     * Render menu for site usage with comprehensive caching
     *
     * @access public
     * @static
     *
     * @param String $menu name of menu called as String
     * @param array $options Styling options passed
     *
     * @return null|bool
     * @throws Exception
     */
    public static function renderMenu(string $menu, array $options = []): ?bool
    {
        global $p;

        // Build cache key based on menu name and context
        $variant = '';
        $hidePharmacyItems =
            ! empty($p->pagedata['Hide Pharmacy Items'][0]['value'])
            || (app('twig')->getGlobals()['hide_pharmacy_items'] ?? false);

        if ($hidePharmacyItems) {
            $variant = '_without_children';
        }

        $cacheKey = self::getCacheKey($menu.':render', $variant);
        $ttl = now()->addHours(6);

        $html = Cache::remember($cacheKey, $ttl, function () use ($menu, $options, $variant) {
            // Resolve the menu with multisite support
            $root_menu = SiteMenu::find(CMS_ROOT_PAGE_ID);
            $mm = new MultisiteManager($root_menu);
            $display_root_menu = $mm->getDisplayEntity();

            $current_menu = null;

            $candidate_menus = self::where('name', $menu)
                ->where('published', 1)
                ->get();

            foreach ($candidate_menus as $candidate_menu) {
                $candidate_root_menu = $candidate_menu->getAncestors()->last();
                if ($candidate_root_menu && $candidate_root_menu->id == $display_root_menu->id) {
                    $current_menu = $candidate_menu;
                    break;
                }
            }

            if (! $current_menu || ! $current_menu->published) {
                return '';
            }

            // Prepare options
            $opts = $options;
            $opts['container_class'] = $current_menu->css_class;
            if (! isset($opts['first_container_class'])) {
                $opts['first_container_class'] = SITE_MENU_FIRST_CONTAINER_CLASS;
            }

            // Build the tree
            $menu_tree = self::fetchTree($current_menu);

            if (! count($menu_tree)) {
                return '';
            }

            // Render the menu HTML
            return self::getMenu($menu.$variant, $menu_tree, $opts);
        });

        echo $html;

        return $html !== '' ? true : false;
    }

    /**
     * SiteMenu::allBrands()
     *
     * Retrieve all brands with caching
     *
     * @access public
     * @return array list of all brands
     */
    public function allBrands()
    {
        $cacheKey = self::getCacheKey('all_brands');
        $ttl = now()->addHours(6);

        return Cache::remember($cacheKey, $ttl, function () {
            return Brand::getAllBrands();
        });
    }

    /**
     * SiteMenu::listCategories()
     *
     * Retrieve list of categories to display with caching
     *
     * @access private
     * @param int $parent_menu_item_id
     * @return array list of categories as an array
     */
    private function listCategories($parent_menu_item_id = 0)
    {
        $cacheKey = self::getCacheKey("categories:{$this->id}:{$this->category_list}:{$parent_menu_item_id}");
        $ttl = now()->addHours(6);

        return Cache::remember($cacheKey, $ttl, function () use ($parent_menu_item_id) {
            if ($this->category_list == -1) {
                $categories = CategoryHelper::getCategoryTree(0, 1);
            } else {
                $categories = CategoryHelper::getCategoryTree($this->category_list, 1);
            }

            // Limit categories to first 35
            $categories = array_slice($categories, 0, 35, true);

            foreach ($categories as $k => $cat) {
                $category = (new Category)->find($cat['id']);

                if (! $category) {
                    continue;
                }

                $hidden = (bool) $category->hide;
                $parentID = 9999 + $category->id;
                $categories[$k]['id'] = $parentID;
                $categories[$k]['parent_id'] = $parent_menu_item_id;
                $categories[$k]['category_id'] = (int) $category->id;
                $categories[$k]['name'] = $category->name;
                $categories[$k]['published'] = ! $hidden ? '1' : '0';
                $categories[$k]['target'] = '_self';
                $categories[$k]['css_class'] = '';

                $sub_tree = [];

                foreach ($categories[$k]['sub_tree'] as $x => $subsub) {
                    $sub_tree[$x]['id'] = 9999 + $subsub['id'];
                    $sub_tree[$x]['parent_id'] = $parentID;
                    $sub_tree[$x]['category_id'] = (int) $subsub['id'];
                    $sub_tree[$x]['name'] = $subsub['name'];
                    $sub_tree[$x]['url'] = $subsub['url'];
                    $hiddensub = (bool) $category->hide;
                    $sub_tree[$x]['published'] = ! $hiddensub ? '1' : '0';
                    $sub_tree[$x]['target'] = '_self';
                    $sub_tree[$x]['css_class'] = '';
                    $sub_tree[$x]['subs'] = [];
                }

                $categories[$k]['subs'] = $sub_tree;
                unset($categories[$k]['sub_tree']);
            }

            if ($categories) {
                foreach ($categories as $k => $category) {
                    // Remove brackets and it's contents from category name
                    $categories[$k]['name'] = trim(preg_replace('/\s*\([^)]*\)/', '', $category['name']));
                }
            }

            return $categories;
        });
    }

    /**
     * Invalidate cache for a specific menu
     *
     * @access public
     * @static
     *
     * @param SiteMenu|array $menu Menu object or array with 'name' key
     *
     * @return void
     */
    public static function invalidateCache($menu): void
    {
        $menuName = is_array($menu) ? $menu['name'] : $menu->name;

        // Clear all cache variations for this menu
        $patterns = [
            self::getCacheKey($menuName.':render'),
            self::getCacheKey($menuName.':render', '_without_children'),
            self::getCacheKey($menuName.'_tree'),
        ];

        foreach ($patterns as $pattern) {
            Cache::forget($pattern);
        }

        // Also clear category caches if this menu has categories
        if (! is_array($menu) && ! empty($menu->category_list)) {
            Cache::forget(self::getCacheKey("categories:{$menu->id}:{$menu->category_list}:0"));
        }

        // Also clear brand cache if this menu has brands
        if (! is_array($menu) && ! empty($menu->brand_list)) {
            Cache::forget(self::getCacheKey('all_brands'));
        }
    }

    /**
     * Invalidate all menu caches across the system
     *
     * @access public
     * @static
     *
     * @return void
     */
    public static function invalidateAllMenuCaches(): void
    {
        // Clear top level menus cache first
        Cache::forget(self::getCacheKey('top_level_menus'));

        // Get menus without using cache
        $menus = self::where('parent_id', 0)->get();

        // Invalidate each menu's cache
        foreach ($menus as $menu) {
            self::invalidateCache($menu);
        }

        // Also clear any category-related caches
        $allMenus = self::all();
        foreach ($allMenus as $menu) {
            if (! empty($menu->category_list)) {
                Cache::forget(self::getCacheKey("categories:{$menu->id}:{$menu->category_list}:0"));
            }
        }
    }

    /**
     * Invalidate cache for a menu and all its ancestors
     *
     * This ensures that when a sub-menu is added/updated, the parent menu's
     * cached tree gets refreshed as well.
     *
     * @access public
     * @static
     *
     * @param SiteMenu $menu_entry The menu entry that was saved
     *
     * @return void
     */
    public static function invalidateMenuAndAncestors($menu_entry): void
    {
        // Clear top level menus cache (needed if menu structure changed)
        Cache::forget(self::getCacheKey('top_level_menus'));

        // Invalidate the menu itself
        self::invalidateCache($menu_entry);

        // Get the root menu by traversing ancestors
        $ancestors = $menu_entry->getAncestors();

        // Invalidate all ancestor caches
        foreach ($ancestors as $ancestor) {
            self::invalidateCache($ancestor);
        }

        // Also invalidate the root menu if this menu has ancestors
        if ($ancestors->count() > 0) {
            $rootMenu = $ancestors->last();
            self::invalidateCache($rootMenu);
        }
    }

    /**
     * SiteMenu::getMenus()
     *
     * Retrieve all top level Site menus with caching
     * Used to get list of menus for use in admin area
     *
     * @access   public
     * @static
     * @return Collection
     *
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     */
    public static function getMenus(): Collection
    {
        $cacheKey = self::getCacheKey('top_level_menus');
        $ttl = now()->addHours(6);

        return Cache::remember($cacheKey, $ttl, function () {
            return self::where('parent_id', 0)->get();
        });
    }

    /**
     * SiteMenu::getSubMenu()
     *
     * Retrieve a collection of child SiteMenus either all or only direct ones
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @param boolean $direct just direct children or all children
     *
     * @return  Collection
     */
    public function getSubMenu($direct = false)
    {
        $sub_menu = new Collection;
        $parent_id = $this->id ? $this->id : 0;

        $sub_items = self::where('parent_id', $parent_id)
            ->orderBy('order', 'ASC')
            ->get();

        foreach ($sub_items as $sub_item) {
            $sub_menu->push($sub_item);
            if (! $direct) {
                $sub_menu = $sub_menu->merge($sub_item->getSubMenu());
            }
        }

        return $sub_menu;
    }

    /**
     * SiteMenu::getSiteMenuTree()
     *
     * Retrieve a collection of child SiteMenus either all or only direct ones
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @param boolean $all all SiteMenus in the tree or only published ones
     *
     * @return  Collection
     */
    public function getSiteMenuTree($all = true)
    {
        $tree = new Collection;

        if ($all) {
            $sub_items = self::where('parent_id', $this->id)
                ->orderBy('order', 'ASC')
                ->get();
        } else {
            $sub_items = self::where('parent_id', $this->id)
                ->where('published', 1)
                ->orderBy('order', 'ASC')
                ->get();
        }

        foreach ($sub_items as $sub_item) {
            $sub_item->subs = $sub_item->getSiteMenuTree($all);

            if (! empty($sub_item->site_tree)) {
                $sub_item->cms_tree = CMSNav::tree($sub_item->site_tree);
            }

            if (! empty($sub_item->brand_list)) {
                $sub_item->sub_brands = self::allBrands();
            }

            if (! empty($sub_item->category_list)) {
                $sub_item->sub_categories = $sub_item->listCategories($sub_item->id);
                if (empty($sub_item->url)) {
                    $sub_item->url = browse_url($sub_item->category_list);
                }
                if (! empty($sub_item->sub_categories)) {
                    $sub_item->subs = $sub_item->sub_categories;
                }
            }

            if (empty($sub_item->url) && ! empty($sub_item->link)) {
                $sub_item->url = $sub_item->link;
            }

            if (empty($sub_item->css_class)) {
                $sub_item->css_class = '';
            }

            $sub_item->parent_id = (int) $sub_item->parent_id;

            $tree->push($sub_item);
        }

        return $tree;
    }

    /**
     * SiteMenu::getAllSiteMenuOptions()
     *
     * Retrieve a string with option tags containing all SiteMenus to use in SiteMenu add/edit form
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @param integer $direct_parent ID of direct parent SiteMenu to mark the correct option tag as selected
     *
     * @static
     * @return  array
     */
    public static function getAllSiteMenuOptions($direct_parent = 0)
    {
        $options = [];
        $options[] = [
            'id' => 0,
            'name' => 'Root',
            'selected' => $direct_parent == 0,
            'sublevel' => 0,
        ];

        foreach (self::where('parent_id', 0)->orderBy('order', 'ASC')->get() as $item) {
            $item->getSiteMenuOptions($options, $direct_parent);
        }

        return $options;
    }

    /**
     * SiteMenu::getSiteMenuOptions()
     *
     * Add items containing child SiteMenus to the $option_array to be used in SiteMenu add/edit form
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @param integer $direct_parent ID of direct parent SiteMenu to mark the correct option tag as selected
     * @param integer $sublevel level of nesting of the submenu
     *
     * @return  void
     */
    public function getSiteMenuOptions(&$option_array, $direct_parent = 0, $sublevel = 0)
    {
        array_push($option_array, [
            'id' => $this->id,
            'name' => $this->name,
            'selected' => $direct_parent == $this->id,
            'sublevel' => $sublevel,
        ]);

        foreach (self::where('parent_id', $this->id)->orderBy('order', 'ASC')->get() as $sub_item) {
            $sub_item->getSiteMenuOptions($option_array, $direct_parent, $sublevel + 1);
        }
    }

    /**
     * SiteMenu::getBreadcrumbs()
     *
     * Retrieve a array with links to be used for breadcrumbs
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @param boolean $end_class flag for adding class="end" for the current SiteMenu
     *
     * @return  array
     */
    public function getBreadcrumbs($end_class = true)
    {
        $breadcrumbs = [];
        $breadcrumbs[] = [
            'url' => '/admin/',
            'title' => 'Home',
            'end_class' => false,
        ];

        if (
            $_SERVER['SCRIPT_NAME'] == '/plugins/SiteMenu/admin/index.php'
            && ! isset($_REQUEST['id'])
            && (! isset($_REQUEST['menu_id']) || $this->id == null)
        ) {
            $manage_end = true;
        } else {
            $manage_end = false;
        }
        $breadcrumbs[] = [
            'url' => '/plugins/SiteMenu/admin/index.php',
            'title' => 'Manage menus',
            'end_class' => $manage_end,
        ];

        foreach ($this->getAncestors()->reverse() as $ancestor) {
            $breadcrumbs[] = [
                'url' => '/plugins/SiteMenu/admin/index.php?id='.$ancestor->id,
                'title' => clean_page($ancestor->name),
                'end_class' => false,
            ];
        }

        $breadcrumbs[] = [
            'url' => ! $end_class ? '/plugins/SiteMenu/admin/index.php?id='.$this->id : '',
            'title' => clean_page($this->name),
            'end_class' => $end_class,
        ];

        if ((! $end_class || $this->id == 0) && $_SERVER['SCRIPT_NAME'] == '/plugins/SiteMenu/admin/edit.php') {
            $breadcrumbs[] = [
                'url' => '',
                'title' => 'Add menu item',
                'end_class' => true,
            ];
        }

        return $breadcrumbs;
    }

    /**
     * SiteMenu::getAncestors()
     *
     * Retrieve a collection of the ancestors of a SiteMenu
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @return  Collection
     */
    public function getAncestors()
    {
        $ancestors = new Collection;

        if ($this->parent_id != 0) {
            $ancestors->push($this->parentMenu);
            $ancestors = $ancestors->merge($this->parentMenu->getAncestors());
        }

        return $ancestors;
    }

    /**
     * SiteMenu::parentMenu()
     *
     * Eloquent relation with the direct parent SiteMenu
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @return  HasOne
     */

    public function parentMenu()
    {
        return $this->hasOne(SiteMenu::class, 'id', 'parent_id');
    }

    /**
     * Relationship with child menu entries
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function children()
    {
        return $this->hasMany(SiteMenu::class, 'parent_id');
    }

    /**
     * SiteMenu::directChildrenCount()
     *
     * Retrieve the number of direct children of a SiteMenu
     * @author   mtc. Dawid Bestrzynski <dawid.bestrzynski@mtcmedia.co.uk>
     *
     * @return  integer
     */
    public function directChildrenCount()
    {
        return self::where('parent_id', $this->id)->count();
    }

    /**
     * Resets all menu caches
     *
     * @return void
     */
    public static function resetCache(): void
    {
        self::invalidateAllMenuCaches();
    }

    public function replicateWithRelations()
    {
        $clone = $this->replicate();
        $clone->save();

        foreach ($this->children as $sub_menu) {
            $sub_menu_clone = $sub_menu->replicateWithRelations();
            $clone->children()->save($sub_menu_clone);
        }

        return $clone;
    }

    public function getAdminUrl()
    {
        $url = "/plugins/SiteMenu/admin/?id={$this->id}";

        return $url;
    }
}
