<?php
/**
 * Site Menu item model.
 *
 * Ported from the legacy Mtc\Plugins\SiteMenu\Classes\SiteMenu class. Holds the
 * self-referencing menu tree, dynamic enrichment (categories / brands / CMS
 * trees) and the caching layer. Rendering lives in SiteMenuRenderer.
 *
 * @package Mtc\Modules\SiteMenu
 */

namespace Mtc\Modules\SiteMenu\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Mtc\Shop\Category;
use Mtc\Shop\Item;
use CategoryHelper;
use Mtc\Shop\Brand;
use CMSNav;

class SiteMenuItem extends Model
{
    use SoftDeletes;

    /**
     * @var string Associated table.
     */
    protected $table = 'site_menu';

    /**
     * @var array List of fillable attributes.
     */
    protected $fillable = [
        'name',
        'url',
        'target',
        'parent_id',
        'category_list',
        'product_list',
        'mega_menu',
        'mega_menu_items',
        'brand_list',
        'site_tree',
        'css_class',
        'published',
        'category_id',
        'single_category',
        'single_cms_page',
    ];

    /**
     * @var array Attribute casts. mega_menu_items is stored as a JSON array of
     * ordered {type, id} refs picked in the admin mega-menu builder.
     */
    protected $casts = [
        'mega_menu_items' => 'array',
    ];

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

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

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

            self::invalidateAllMenuCaches();
        });

        self::saved(function ($menu_entry) {
            // 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);
        });
    }

    /**
     * Generate cache key for a menu.
     *
     * Public so SiteMenuRenderer can build render cache keys.
     *
     * @param string $menuName
     * @param string $variant Optional variant identifier (e.g., '_without_children')
     * @return string
     */
    public 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 SiteMenuItem $menu
     * @return Collection
     */
    public static function fetchTree($menu): Collection
    {
        $cacheKey = self::getCacheKey($menu->name . '_tree');
        $ttl = now()->addHours(config('site_menu.cache_ttl_hours', 6));

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

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

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

    /**
     * Retrieve list of categories to display with caching.
     *
     * @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(config('site_menu.cache_ttl_hours', 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;
        });
    }

    /**
     * Retrieve list of a category's products to display as menu links, cached.
     *
     * Mirrors listCategories(): each product becomes a leaf menu node (no
     * children) pointing at the product page. Only active (published, not
     * deleted) products are pulled through, capped at site_menu.max_products so
     * a large category cannot flood the navigation.
     *
     * @param int $parent_menu_item_id id of the owning menu item (becomes parent_id)
     * @return array list of products as menu-node arrays
     */
    private function listProducts($parent_menu_item_id = 0)
    {
        $cacheKey = self::getCacheKey("products:{$this->id}:{$this->product_list}:{$parent_menu_item_id}");
        $ttl = now()->addHours(config('site_menu.cache_ttl_hours', 6));

        return Cache::remember($cacheKey, $ttl, function () use ($parent_menu_item_id) {
            $category = (new Category)->find($this->product_list);

            if (!$category) {
                return [];
            }

            $limit = (int) config('site_menu.max_products', 20);

            // unique('id') guards against duplicate items_categories pivot rows
            // producing the same product twice in the menu.
            $products = $category->items()
                ->active()
                ->orderBy('items.name', 'ASC')
                ->get()
                ->unique('id')
                ->take($limit);

            $items = [];

            foreach ($products as $product) {
                if (empty($product->url)) {
                    continue;
                }

                $items[] = [
                    'id'         => 'product_' . $product->id,
                    'parent_id'  => $parent_menu_item_id,
                    'name'       => $product->name,
                    'url'        => $product->url,
                    'published'  => '1',
                    'target'     => '_self',
                    'css_class'  => '',
                    'subs'       => [],
                ];
            }

            return $items;
        });
    }

    /**
     * Build the hand-picked mega-menu children as menu-node arrays, cached.
     *
     * Unlike listCategories()/listProducts() (which auto-list all children of a
     * single source category), this resolves the explicit, ordered list of
     * category/product refs stored in mega_menu_items. Each ref becomes a leaf
     * menu node in the same shape the render template expects. Order is
     * preserved; missing/hidden entries are skipped.
     *
     * @param int $parent_menu_item_id id of the owning menu item (becomes parent_id)
     * @return array list of picked items as menu-node arrays
     */
    private function listMegaMenuItems($parent_menu_item_id = 0)
    {
        $cacheKey = self::getCacheKey("mega:{$this->id}:{$parent_menu_item_id}");
        $ttl = now()->addHours(config('site_menu.cache_ttl_hours', 6));

        return Cache::remember($cacheKey, $ttl, function () use ($parent_menu_item_id) {
            $refs = is_array($this->mega_menu_items) ? $this->mega_menu_items : [];
            $items = [];

            foreach ($refs as $ref) {
                $type = $ref['type'] ?? null;
                $id = (int) ($ref['id'] ?? 0);

                if (!$id) {
                    continue;
                }

                if ($type === 'category') {
                    $category = (new Category)->find($id);
                    if (!$category || (bool) $category->hide) {
                        continue;
                    }
                    $items[] = [
                        'id'         => 'category_' . $category->id,
                        'parent_id'  => $parent_menu_item_id,
                        'name'       => $category->name,
                        'url'        => browse_url($category->id),
                        'published'  => '1',
                        'target'     => '_self',
                        'css_class'  => '',
                        'subs'       => [],
                    ];
                } elseif ($type === 'product') {
                    $product = Item::query()->active()->find($id);
                    if (!$product || empty($product->url)) {
                        continue;
                    }
                    $items[] = [
                        'id'         => 'product_' . $product->id,
                        'parent_id'  => $parent_menu_item_id,
                        'name'       => $product->name,
                        'url'        => $product->url,
                        'published'  => '1',
                        'target'     => '_self',
                        'css_class'  => '',
                        'subs'       => [],
                    ];
                }
            }

            return $items;
        });
    }

    /**
     * Invalidate cache for a specific menu.
     *
     * @param SiteMenuItem|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 product caches if this menu lists a category's products
        if (!is_array($menu) && !empty($menu->product_list)) {
            Cache::forget(self::getCacheKey("products:{$menu->id}:{$menu->product_list}:{$menu->id}"));
        }

        // Also clear the hand-picked mega-menu cache if this menu uses one
        if (!is_array($menu) && !empty($menu->mega_menu)) {
            Cache::forget(self::getCacheKey("mega:{$menu->id}:{$menu->id}"));
        }

        // 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.
     *
     * @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"));
            }
            if (!empty($menu->product_list)) {
                Cache::forget(self::getCacheKey("products:{$menu->id}:{$menu->product_list}:{$menu->id}"));
            }
            if (!empty($menu->mega_menu)) {
                Cache::forget(self::getCacheKey("mega:{$menu->id}:{$menu->id}"));
            }
        }
    }

    /**
     * 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.
     *
     * @param SiteMenuItem $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);
        }
    }

    /**
     * Retrieve all top level Site menus with caching.
     * Used to get list of menus for use in admin area.
     *
     * @return Collection
     */
    public static function getMenus(): Collection
    {
        $cacheKey = self::getCacheKey('top_level_menus');
        $ttl = now()->addHours(config('site_menu.cache_ttl_hours', 6));

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

    /**
     * Retrieve a collection of child menu items, either all or only direct ones.
     *
     * @param bool $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;
    }

    /**
     * Retrieve the menu tree beneath this item, with dynamic enrichment.
     *
     * @param bool $all all items 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->mega_menu) && !empty($sub_item->mega_menu_items)) {
                // Hand-picked mega menu: children come from the explicit,
                // ordered list of categories/products chosen in the admin
                // builder, replacing the auto-listing branches below.
                $sub_item->subs = $sub_item->listMegaMenuItems($sub_item->id);
            } elseif (!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->mega_menu) && !empty($sub_item->product_list)) {
                $sub_item->sub_products = $sub_item->listProducts($sub_item->id);
                if (empty($sub_item->url)) {
                    $sub_item->url = browse_url($sub_item->product_list);
                }
                if (!empty($sub_item->sub_products)) {
                    // Append products after any existing children (manual
                    // sub-items or the sub-categories pulled in above) so a
                    // category can list both its sub-categories and products.
                    $existing = $sub_item->subs instanceof Collection
                        ? $sub_item->subs->all()
                        : (array) $sub_item->subs;
                    $sub_item->subs = array_merge($existing, $sub_item->sub_products);
                }
            }

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

    /**
     * Retrieve an array of option data containing all menus for the add/edit form.
     *
     * @param int $direct_parent ID of direct parent to mark as selected
     * @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;
    }

    /**
     * Append this item and its descendants to the option array for the add/edit form.
     *
     * @param array $option_array
     * @param int $direct_parent ID of direct parent to mark as selected
     * @param int $sublevel level of nesting
     * @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);
        }
    }

    /**
     * Retrieve an array with links to be used for breadcrumbs.
     *
     * @param bool $end_class flag for adding class="end" for the current item
     * @return array
     */
    public function getBreadcrumbs($end_class = true)
    {
        $breadcrumbs   = [];
        $breadcrumbs[] = [
            'url'       => '/admin/',
            'title'     => 'Home',
            'end_class' => false,
        ];

        $breadcrumbs[] = [
            'url'       => '/admin/site-menu',
            'title'     => 'Manage menus',
            'end_class' => $this->id == null,
        ];

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

        if ($this->id !== null) {
            $breadcrumbs[] = [
                'url'       => !$end_class ? '/admin/site-menu?id=' . $this->id : '',
                'title'     => clean_page($this->name),
                'end_class' => $end_class,
            ];
        }

        return $breadcrumbs;
    }

    /**
     * Retrieve a collection of the ancestors of this item.
     *
     * @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;
    }

    /**
     * Eloquent relation with the direct parent item.
     */
    public function parentMenu()
    {
        return $this->hasOne(self::class, 'id', 'parent_id');
    }

    /**
     * Relationship with child menu entries.
     */
    public function children()
    {
        return $this->hasMany(self::class, 'parent_id');
    }

    /**
     * Retrieve the number of direct children of this item.
     *
     * @return int
     */
    public function directChildrenCount()
    {
        return self::where('parent_id', $this->id)->count();
    }

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

    /**
     * Deep-clone this item and all of its descendants.
     */
    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;
    }

    /**
     * Admin edit URL for this item.
     */
    public function getAdminUrl()
    {
        return "/admin/site-menu?id={$this->id}";
    }
}
