<?php

namespace App\Services\Menu;

use App\Models\User;
use Illuminate\Support\Facades\File;

/**
 * Service for collecting and managing dynamic module menus.
 *
 * Modules register their menus using the RegistersMenuItems trait in their
 * service provider's boot() method. This service collects all registered
 * menus and builds the final menu structure.
 *
 * Module status is checked by reading modules_statuses.json directly,
 * bypassing nwidart/laravel-modules caches for reliable behavior after
 * web-based module activation/deactivation.
 */
class MenuRegistrarService implements IMenuRegistrarService
{
    /**
     * Registered menu items grouped by module.
     *
     * @var array<string, array{priority: int, items: array<MenuItemDTO>}>
     */
    private array $registeredMenus = [];

    /**
     * Base menu items (Dashboard, etc.) loaded from JSON.
     *
     * @var array<array<string, mixed>>
     */
    private array $baseMenu = [];

    /**
     * Cached enabled modules from file (reset per request).
     *
     * @var array<string, bool>|null
     */
    private ?array $enabledModulesCache = null;

    /**
     * Cached tenant allowed modules (reset per request).
     *
     * @var array<string>|null
     */
    private ?array $tenantAllowedModulesCache = null;

    /**
     * {@inheritdoc}
     */
    public function register(string $module, array $items, int $priority = 100): void
    {
        $menuItems = [];
        foreach ($items as $item) {
            $menuItems[] = $item instanceof MenuItemDTO
                ? $item
                : MenuItemDTO::fromArray($item);
        }

        $this->registeredMenus[$module] = [
            'priority' => $priority,
            'items' => $menuItems,
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function registerHeader(string $module, string $header, int $priority = 100): void
    {
        $headerItem = new MenuItemDTO(
            name: '',
            menuHeader: $header,
            priority: $priority
        );

        if (! isset($this->registeredMenus[$module])) {
            $this->registeredMenus[$module] = [
                'priority' => $priority,
                'items' => [],
            ];
        }

        // Add header at the beginning of the module's items
        array_unshift($this->registeredMenus[$module]['items'], $headerItem);
    }

    /**
     * Set base menu items (loaded from baseMenu.json).
     *
     * @param  array<array<string, mixed>>  $items
     */
    public function setBaseMenu(array $items): void
    {
        $this->baseMenu = $items;
    }

    /**
     * {@inheritdoc}
     */
    public function getAll(): array
    {
        return $this->buildMenu();
    }

    /**
     * {@inheritdoc}
     */
    public function getForRole(string $role): array
    {
        return $this->buildMenu([$role]);
    }

    /**
     * {@inheritdoc}
     */
    public function getForUser(User $user): array
    {
        $roles = $user->getRoleNames()->toArray();

        return $this->buildMenu($roles);
    }

    /**
     * {@inheritdoc}
     */
    public function toSearchArray(User $user): array
    {
        $menu = $this->getForUser($user);
        $pages = [];

        foreach ($menu as $item) {
            $this->extractSearchableItems($item, $pages);
        }

        return $pages;
    }

    /**
     * {@inheritdoc}
     */
    public function hasRegisteredMenus(): bool
    {
        return ! empty($this->registeredMenus);
    }

    /**
     * {@inheritdoc}
     */
    public function clear(): void
    {
        $this->registeredMenus = [];
        $this->baseMenu = [];
        $this->enabledModulesCache = null;
        $this->tenantAllowedModulesCache = null;
    }

    /**
     * Check if a module is enabled and accessible.
     *
     * This checks:
     * 1. Module is enabled in modules_statuses.json
     * 2. In SaaS mode with tenant context: tenant's plan includes the module
     */
    private function isModuleEnabled(string $moduleName): bool
    {
        if ($this->enabledModulesCache === null) {
            $this->enabledModulesCache = $this->loadEnabledModules();
        }

        // First check if module is enabled globally
        if (! ($this->enabledModulesCache[$moduleName] ?? false)) {
            return false;
        }

        // In SaaS tenant context, also check tenant plan access
        if (! $this->tenantHasModuleAccess($moduleName)) {
            return false;
        }

        return true;
    }

    /**
     * Check if the current tenant has access to a module.
     *
     * Returns true if:
     * - Not in SaaS mode
     * - No tenant context (central domain)
     * - Module is a core module
     * - Tenant's plan includes the module
     */
    private function tenantHasModuleAccess(string $moduleName): bool
    {
        // Skip if not in SaaS mode
        if (! function_exists('isSaaSMode') || ! isSaaSMode()) {
            return true;
        }

        // Skip if no tenant context
        if (! function_exists('isTenant') || ! isTenant()) {
            return true;
        }

        // Core modules are always allowed
        if (function_exists('isCoreModule') && isCoreModule($moduleName)) {
            return true;
        }

        // Load tenant allowed modules if not cached
        if ($this->tenantAllowedModulesCache === null) {
            $this->tenantAllowedModulesCache = $this->loadTenantAllowedModules();
        }

        // If null returned, tenant has unlimited access
        if ($this->tenantAllowedModulesCache === null) {
            return true;
        }

        return in_array($moduleName, $this->tenantAllowedModulesCache, true);
    }

    /**
     * Load the list of modules allowed by the tenant's subscription plan.
     *
     * @return array<string>|null List of allowed modules, or null if unlimited
     */
    private function loadTenantAllowedModules(): ?array
    {
        // Get tenant from app container
        if (! app()->has('tenant')) {
            return null; // No restriction
        }

        $tenant = app('tenant');

        if (! $tenant || ! method_exists($tenant, 'activeSubscription')) {
            return null; // No restriction
        }

        // Get active subscription with plan
        $subscription = $tenant->activeSubscription()->with('plan')->first();

        if (! $subscription || ! $subscription->plan) {
            return []; // No subscription = no addon access
        }

        // Get allowed modules from plan restrictions
        $allowedModules = $subscription->plan->restrictions['modules'] ?? null;

        // null means unlimited access to all modules
        if ($allowedModules === null) {
            return null;
        }

        return $allowedModules;
    }

    /**
     * Load enabled modules directly from modules_statuses.json.
     *
     * @return array<string, bool>
     */
    private function loadEnabledModules(): array
    {
        $statusFile = base_path('modules_statuses.json');

        if (! File::exists($statusFile)) {
            return [];
        }

        $content = File::get($statusFile);
        $statuses = json_decode($content, true);

        if (! is_array($statuses)) {
            return [];
        }

        $enabled = [];
        foreach ($statuses as $module => $status) {
            $enabled[$module] = $status === true;
        }

        return $enabled;
    }

    /**
     * Build the complete menu, sorted by priority and filtered.
     *
     * @param  array<string>|null  $roles  Roles to filter by (null = no filtering)
     * @return array<array<string, mixed>>
     */
    private function buildMenu(?array $roles = null): array
    {
        $menu = [];

        // Add base menu items first
        foreach ($this->baseMenu as $item) {
            if ($this->isItemVisible($item, $roles)) {
                $menu[] = $item;
            }
        }

        // Sort registered menus by priority
        $sortedMenus = $this->registeredMenus;
        uasort($sortedMenus, fn ($a, $b) => $a['priority'] <=> $b['priority']);

        // First pass: Collect all items and filter them
        /** @var array<string, int> $parentIndex menuKey => index in $allItems */
        $parentIndex = [];
        /** @var array<array{item: array<string, mixed>, priority: int, module: string}> $allItems */
        $allItems = [];

        foreach ($sortedMenus as $module => $data) {
            // Check if module is enabled using direct file read
            if (! $this->isModuleEnabled($module)) {
                continue;
            }

            foreach ($data['items'] as $item) {
                $filteredItem = $this->filterMenuItem($item, $roles);
                if ($filteredItem === null) {
                    continue;
                }

                $itemData = [
                    'item' => $filteredItem,
                    'priority' => $data['priority'],
                    'module' => $module,
                ];

                $allItems[] = $itemData;

                // Index items with menuKey as potential parents
                if (isset($filteredItem['menuKey'])) {
                    $parentIndex[$filteredItem['menuKey']] = count($allItems) - 1;
                }
            }
        }

        // Second pass: Merge items with parentKey into their parent's submenu
        $childIndices = [];

        foreach ($allItems as $idx => $itemData) {
            $item = $itemData['item'];
            $parentKey = $item['parentKey'] ?? null;

            if ($parentKey !== null && isset($parentIndex[$parentKey])) {
                $childIndices[$idx] = true;

                $parentIdx = $parentIndex[$parentKey];
                if (! isset($allItems[$parentIdx]['item']['submenu'])) {
                    $allItems[$parentIdx]['item']['submenu'] = [];
                }

                // Merge child's slugs into parent for active state detection
                $this->mergeChildSlugsIntoParent($allItems[$parentIdx]['item'], $item);

                unset($item['parentKey']);
                $allItems[$parentIdx]['item']['submenu'][] = $item;
            }
        }

        // Third pass: Collect top-level items
        foreach ($allItems as $idx => $itemData) {
            if (isset($childIndices[$idx])) {
                continue;
            }
            $item = $itemData['item'];
            unset($item['parentKey']);
            $menu[] = $item;
        }

        return $menu;
    }

    /**
     * Filter a menu item based on roles and addon status.
     *
     * @param  array<string>|null  $roles
     * @return array<string, mixed>|null
     */
    private function filterMenuItem(MenuItemDTO $item, ?array $roles): ?array
    {
        // Check tenant context for MultiTenancyCore menus
        if ($item->addon === 'MultiTenancyCore') {
            if (function_exists('isTenant') && isTenant()) {
                return null;
            }
        }

        // Check role visibility
        if ($roles !== null && ! $item->isVisibleForRoles($roles)) {
            return null;
        }

        // Check addon visibility using direct file check
        if ($item->addon !== null && ! $this->isModuleEnabled($item->addon)) {
            return null;
        }

        $result = $item->toArray();

        // Filter submenu items recursively
        if (! empty($item->submenu)) {
            $filteredSubmenu = [];
            foreach ($item->submenu as $subItem) {
                $subDto = $subItem instanceof MenuItemDTO
                    ? $subItem
                    : MenuItemDTO::fromArray($subItem);

                $filteredSub = $this->filterMenuItem($subDto, $roles);
                if ($filteredSub !== null) {
                    $filteredSubmenu[] = $filteredSub;
                }
            }

            if (empty($filteredSubmenu)) {
                if ($item->url === null && ! $item->isHeader()) {
                    return null;
                }
            }

            $result['submenu'] = $filteredSubmenu;
        }

        return $result;
    }

    /**
     * Check if a raw menu item (array) is visible for the given roles.
     *
     * @param  array<string, mixed>  $item
     * @param  array<string>|null  $roles
     */
    private function isItemVisible(array $item, ?array $roles): bool
    {
        // Check addon using direct file check
        if (isset($item['addon']) && ! $this->isModuleEnabled($item['addon'])) {
            return false;
        }

        // Check roles
        if ($roles !== null && isset($item['roles']) && ! empty($item['roles'])) {
            if (empty(array_intersect($roles, $item['roles']))) {
                return false;
            }
        }

        return true;
    }

    /**
     * Extract searchable items from menu recursively.
     *
     * @param  array<string, mixed>  $item
     * @param  array<array{name: string, url: string, icon: string|null}>  $pages
     */
    private function extractSearchableItems(array $item, array &$pages): void
    {
        if (isset($item['menuHeader'])) {
            return;
        }

        if (isset($item['submenu']) && is_array($item['submenu'])) {
            foreach ($item['submenu'] as $subItem) {
                $this->extractSearchableItems($subItem, $pages);
            }
        } elseif (isset($item['url']) && $item['url'] !== 'javascript:;') {
            $url = $item['url'];
            if (str_starts_with($url, '/')) {
                $url = substr($url, 1);
            }

            $pages[] = [
                'name' => $item['name'] ?? '',
                'url' => $url,
                'icon' => $item['icon'] ?? 'bx-file',
            ];
        }
    }

    /**
     * Merge child item's slugs into parent's slug array.
     *
     * @param  array<string, mixed>  $parent
     * @param  array<string, mixed>  $child
     */
    private function mergeChildSlugsIntoParent(array &$parent, array $child): void
    {
        $childSlugs = $this->collectSlugsFromItem($child);

        if (empty($childSlugs)) {
            return;
        }

        if (! isset($parent['slug'])) {
            $parent['slug'] = [];
        } elseif (is_string($parent['slug'])) {
            $parent['slug'] = [$parent['slug']];
        }

        $parent['slug'] = array_unique(array_merge($parent['slug'], $childSlugs));
    }

    /**
     * Collect all slugs from a menu item recursively.
     *
     * @param  array<string, mixed>  $item
     * @return array<string>
     */
    private function collectSlugsFromItem(array $item): array
    {
        $slugs = [];

        if (isset($item['slug'])) {
            if (is_array($item['slug'])) {
                $slugs = array_merge($slugs, $item['slug']);
            } else {
                $slugs[] = $item['slug'];
            }
        }

        if (isset($item['submenu']) && is_array($item['submenu'])) {
            foreach ($item['submenu'] as $subItem) {
                $slugs = array_merge($slugs, $this->collectSlugsFromItem($subItem));
            }
        }

        return $slugs;
    }
}
