<?php
/**
 * Menu Data API
 *
 * REST endpoints responsible for delivering WordPress admin menu data to the
 * FleekDash React application.
 *
 * @package FleekDash
 * @since V2.2
 */

namespace FleekDash\API;

use WP_REST_Request;
use WP_REST_Response;
use FleekDash\Admin\Menu\MenuCaptureService;
use FleekDash\Admin\Menu\AdminMenuContextBuilder;
use FleekDash\Admin\Menu\MenuOverridesService;
use FleekDash\Core\AuthMiddleware;
use FleekDash\API\MenuRolePayloadService;
use FleekDash\API\MenuCurrentUserPayloadService;
use FleekDash\Admin\Menu\MenuCapabilityImpactService;
use FleekDash\Admin\Menu\MenuOverridesApplier;
use FleekDash\Admin\Menu\MenuRoleProjectionService;

class MenuDataAPI // NOSONAR php:S1142 - Orchestration class; further split would require MenuIntegrationEnricher, MenuDiagnosticBuilder
{
    private const REST_NAMESPACE = 'fleekdash/v1';
    private const MSG_NOT_AUTHENTICATED = 'Not authenticated';

    /**
     * Fast-path thresholds: when a fresh per-user snapshot has at least this many top-level items
     * AND this many total submenu rows, skip the full wp-admin rebuild in REST and return the
     * snapshot directly. Values chosen to cover a minimal core admin (Dashboard + Posts + Media +
     * Pages + Comments + Appearance + Plugins + Users + Tools + Settings → 10 tops / ~25 submenu rows)
     * with margin for trimmed roles / Multisite.
     */
    private const FAST_PATH_MIN_MENU_ITEMS = 8;
    private const FAST_PATH_MIN_SUBMENU_ROWS = 15;

    private MenuCaptureService $menuCapture;
    private MenuOverridesAPI $menuOverridesApi;
    private MenuOverridesService $menuOverridesService;
    private MenuNewTabUrlsAPI $menuNewTabUrlsApi;
    private MenuDebugAPI $menuDebugApi;
    private AdminMenuContextBuilder $adminMenuContextBuilder;
    private MenuDiagnosticService $menuDiagnosticService;
    private MenuPhpMenuService $menuPhpMenuService;
    private MenuCacheLifecycleService $menuCacheLifecycleService;
    private MenuRolePayloadService $menuRolePayloadService;
    private MenuCurrentUserPayloadService $menuCurrentUserPayloadService;
    private MenuCapabilityImpactService $menuCapabilityImpactService;
    private MenuMergeStrategy $menuMergeStrategy;
    private const OVERRIDES_RESET_FLAG = 'fleekdash_menu_overrides_v2_reset_done';

    public function __construct()
    {
        $this->menuMergeStrategy = new MenuMergeStrategy();
        $this->menuCapture = new MenuCaptureService();
        $this->adminMenuContextBuilder = new AdminMenuContextBuilder();
        $this->menuCurrentUserPayloadService = new MenuCurrentUserPayloadService(
            [MenuItemTransformHelper::class, 'sortMenuItemsByPosition'],
        );
        $this->menuOverridesApi = new MenuOverridesAPI();
        $this->menuOverridesService = new MenuOverridesService();
        $this->menuDiagnosticService = new MenuDiagnosticService();
        $this->menuPhpMenuService = new MenuPhpMenuService($this->adminMenuContextBuilder);
        $this->menuCacheLifecycleService = new MenuCacheLifecycleService($this->menuCapture);
        $this->menuRolePayloadService = new MenuRolePayloadService(
            $this->menuOverridesService,
            new MenuOverridesApplier(),
            new MenuRoleProjectionService(),
        );
        $this->menuCapabilityImpactService = new MenuCapabilityImpactService();
        $this->menuNewTabUrlsApi = new MenuNewTabUrlsAPI();
        $this->menuDebugApi = new MenuDebugAPI();
        $this->resetLegacyOverridesOnce();
    }

    /**
* REQUIRED: All endpoints must use [$this, 'checkPermissions'].
     * Always return true - AuthMiddleware handles auth inside each handler with graceful fallback.
     *
     * This prevents WordPress from validating cookie nonce before reaching our code.
     * WordPress REST API validates cookie nonce automatically when a logged-in cookie is present,
     * which can cause 403 errors even when our permission_callback returns true.
     * By returning true immediately, we bypass WordPress's automatic nonce validation and
     * let AuthMiddleware handle authentication properly inside the handler methods.
     *
     * Handlers must call AuthMiddleware (or delegate to a method that does) before any sensitive
     * side effects. {@see refreshMenu()} authenticates before clearing in-memory capture.
     */
    public function checkPermissions(): bool
    {
        // Return true immediately to bypass WordPress cookie nonce validation
        // WordPress will validate nonce automatically if this returns false or if there's any delay.
        // By returning true here, we ensure the request reaches our handler where AuthMiddleware
        // can properly validate authentication using cookies without nonce validation.
        //
        // SECURITY CHECKLIST: Every handler method must call AuthMiddleware before accessing data.
        // - getMenu()              → AuthMiddleware::check('authenticated')
        // - getMenuByRole()        → AuthMiddleware::check('authenticated')
        // - refreshMenu()          → AuthMiddleware::check('authenticated')
        // - clearMenuCache()       → AuthMiddleware::check('admin')
        // - invalidateSnapshots()  → AuthMiddleware::check('authenticated')
        // - getPhpMenu()           → AuthMiddleware::check('authenticated')
        // - getCacheStatus()       → AuthMiddleware::check('authenticated')
        // - markCacheCleared()     → AuthMiddleware::check('authenticated')
        // - getMenuDiagnostic()    → AuthMiddleware::check('authenticated')
        // MenuOverridesAPI / MenuNewTabUrlsAPI / MenuDebugAPI handle their own auth internally.
        return true;
    }

    /**
     * Register REST API routes
     *
     * IMPORTANT: Register specific routes BEFORE dynamic routes!
     * Otherwise /menu/cache-status gets caught by /menu/{role}
     */
    public function registerRoutes(): void
    {
        $ns = self::REST_NAMESPACE;
        $perm = [$this, 'checkPermissions'];

        // ============================================
        // SPECIFIC ROUTES - MUST BE FIRST!
        // ============================================

        \register_rest_route($ns, '/menu/refresh', [
            'methods' => 'POST',
            'callback' => [$this, 'refreshMenu'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/cache', [
            'methods' => 'DELETE',
            'callback' => [$this, 'clearMenuCache'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/cache-status', [
            'methods' => 'GET',
            'callback' => [$this, 'getCacheStatus'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/diagnostic', [
            'methods' => 'GET',
            'callback' => [$this, 'getMenuDiagnostic'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/cache-cleared', [
            'methods' => 'POST',
            'callback' => [$this, 'markCacheCleared'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/invalidate-snapshots', [
            'methods' => 'POST',
            'callback' => [$this, 'invalidateSnapshots'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/overrides/(?P<role>[a-zA-Z0-9_-]+)', [
            'methods' => 'POST',
            'callback' => [$this->menuOverridesApi, 'saveRoleOverrides'],
            'permission_callback' => $perm,
            'args' => [
                'role' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field'
                ]
            ]
        ]);

        \register_rest_route($ns, '/menu/overrides/(?P<role>[a-zA-Z0-9_-]+)', [
            'methods' => 'GET',
            'callback' => [$this->menuOverridesApi, 'getRoleOverrides'],
            'permission_callback' => $perm,
            'args' => [
                'role' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field'
                ]
            ]
        ]);

        \register_rest_route($ns, '/menu/overrides/(?P<role>[a-zA-Z0-9_-]+)/reset', [
            'methods' => 'POST',
            'callback' => [$this->menuOverridesApi, 'resetRoleOverrides'],
            'permission_callback' => $perm,
            'args' => [
                'role' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field'
                ]
            ]
        ]);

        \register_rest_route($ns, '/menu/overrides/debug/complete', [
            'methods' => 'GET',
            'callback' => [$this->menuDebugApi, 'getCompleteDebugStructure'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/new-tab-urls', [
            'methods' => 'GET',
            'callback' => [$this->menuNewTabUrlsApi, 'getNewTabUrls'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/new-tab-urls', [
            'methods' => 'POST',
            'callback' => [$this->menuNewTabUrlsApi, 'updateNewTabUrls'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/new-tab-urls/reset', [
            'methods' => 'POST',
            'callback' => [$this->menuNewTabUrlsApi, 'resetNewTabUrls'],
            'permission_callback' => $perm
        ]);

        \register_rest_route($ns, '/menu/(?P<role>[a-zA-Z0-9_-]+)/capability-impact', [
            'methods' => 'GET',
            'callback' => [$this, 'getCapabilityImpact'],
            'permission_callback' => $perm,
            'args' => [
                'role' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_text_field'
                ],
                'capability' => [
                    'required' => true,
                    'type' => 'string',
                    'sanitize_callback' => 'sanitize_key'
                ]
            ]
        ]);

        \register_rest_route($ns, '/menu/php', [
            'methods' => 'GET',
            'callback' => [$this, 'getPhpMenu'],
            'permission_callback' => $perm
        ]);

        // ============================================
        // DYNAMIC ROUTES - MUST BE LAST!
        // ============================================

        \register_rest_route($ns, '/menu', [
            'methods' => 'GET',
            'callback' => [$this, 'getMenu'],
            'permission_callback' => [AuthMiddleware::class, 'createAuthenticatedPermissionCallback'],
        ]);

        \register_rest_route($ns, '/menu/(?P<role>[a-zA-Z0-9_-]+)', [
            'methods' => 'GET',
            'callback' => [$this, 'getMenuByRole'],
            'permission_callback' => [AuthMiddleware::class, 'createAuthenticatedPermissionCallback'],
            'args' => [
                'role' => [
                    'required' => true,
                    'type' => 'string',
                    'description' => 'WordPress role slug (administrator, editor, author, contributor, subscriber, or custom role)',
                    'sanitize_callback' => 'sanitize_text_field'
                ]
            ]
        ]);

        // Toolbar routes are handled by ToolbarDataAPI
    }

    /**
     * Get menu for current user
     *
     * Raw `$submenu` is merged per parent (richer branch wins; ties → globals). Top-level `$menu`
     * is merged per slug (core slugs from snapshot for wp-admin labels; non-core from globals).
     */
    public function getMenu(): WP_REST_Response
    {
        try {
            $auth = AuthMiddleware::check('authenticated');
            if (empty($auth['authenticated'])) {
                return $this->menuResponse([
                    'items' => [],
                    'menu_hash' => MenuPayloadHash::EMPTY,
                    'debug' => ['error' => self::MSG_NOT_AUTHENTICATED],
                ]);
            }

            [$rawMenu, $rawSubmenu, $rawSource, $rawResolutionMeta, $snapshotMenuForLabels] = $this->resolveRawMenuAndSubmenu();
            $snapshotRowBySlug = $this->menuMergeStrategy->buildSnapshotMenuRowBySlug($snapshotMenuForLabels);
            $menu_items = $this->menuCurrentUserPayloadService->buildMenuItems($rawMenu, $rawSubmenu, $snapshotRowBySlug);
            $roleSlug = $this->menuRolePayloadService->resolveCurrentUserRoleSlug();
            if ($roleSlug !== '') {
                $menu_items = $this->menuRolePayloadService->applyRoleOverridesToMenuItems(
                    $menu_items,
                    $roleSlug,
                );
            }
            $rawSlugs = $this->extractRawTopLevelSlugs($rawMenu);
            $rawActionableCount = $this->countRawActionableTopLevelItems($rawMenu);
            $formattedSlugs = $this->extractFormattedSourceSlugs($menu_items);
            $missingSlugs = $this->calculateMissingSlugs($rawSlugs, $formattedSlugs);
            $restoredCoreSlugs = $this->extractRestoredCoreSlugs($menu_items);
            $restoredPostTypeSlugs = $this->extractRestoredPostTypeSlugs($menu_items);
            $rawDuplicateSubmenuCount = $this->countRawDuplicateSubmenuEntries($rawSubmenu);
            $integrationCount = $this->countIntegrationTopLevelItems($menu_items);
            $wpFormattedCount = \max(0, \count($menu_items) - $integrationCount);

            return $this->menuResponse([
                'items' => $menu_items,
                'menu_hash' => MenuPayloadHash::hashNormalizedMenu($menu_items),
                'debug' => [
                    'source' => $rawSource,
                    'raw_menu_resolution' => $rawResolutionMeta,
                    'raw_menu_count' => \count($rawMenu),
                    'raw_submenu_count' => \count($rawSubmenu),
                    'formatted_count' => count($menu_items),
                    'raw_slug_count' => \count($rawSlugs),
                    'formatted_source_slug_count' => \count($formattedSlugs),
                    'missing_slug_count' => \count($missingSlugs),
                    'missing_slugs_sample' => \array_slice($missingSlugs, 0, 20),
                    'restored_core_count' => \count($restoredCoreSlugs),
                    'restored_core_slugs' => $restoredCoreSlugs,
                    'restored_post_type_count' => \count($restoredPostTypeSlugs),
                    'restored_post_type_slugs' => $restoredPostTypeSlugs,
                    'raw_duplicate_submenu_count' => $rawDuplicateSubmenuCount,
                    'raw_actionable_top_level_count' => $rawActionableCount,
                    'formatted_wp_top_level_count' => $wpFormattedCount,
                    'formatted_integration_top_level_count' => $integrationCount,
                ],
            ]);
        } catch (\Throwable $e) {
            error_log('FleekDash MenuDataAPI Error: ' . $e->getMessage());
            error_log('FleekDash MenuDataAPI Error (Stack): ' . $e->getTraceAsString());
            return $this->menuResponse([
                'items' => [],
                'menu_hash' => MenuPayloadHash::EMPTY,
                'debug' => ['error' => 'Failed to generate menu', 'message' => $e->getMessage()],
            ]);
        }
    }

    /**
     * Same as resolveRawMenuAndSubmenu but only menu/submenu (role projection builds its own debug).
     * Role preview must avoid stale snapshot fallback to prevent degraded trees when switching roles
     * in Menu Manager. If no fresh snapshot exists, merge strategy will rely on current globals.
     *
     * @return array{0: array<int,mixed>, 1: array<string,mixed>}
     */
    private function resolveRawMenuAndSubmenuForRole(): array
    {
        $this->adminMenuContextBuilder->ensureLoaded();
        global $menu, $submenu;

        $globalsMenu = \is_array($menu) ? $menu : [];
        $globalsSubmenu = \is_array($submenu) ? $submenu : [];

        $resolver = new \FleekDash\Admin\Menu\MenuSourceResolver($this->menuCapture);
        [$snapshotMenu, $snapshotSubmenu, $snapshotSource] = $resolver->resolve(false);
        $snapshotMenu = \is_array($snapshotMenu) ? $snapshotMenu : [];
        $snapshotSubmenu = \is_array($snapshotSubmenu) ? $snapshotSubmenu : [];

        [$rawMenu, $rawSubmenu] = $this->menuMergeStrategy->mergeGlobalsWithSnapshot(
            $globalsMenu,
            $globalsSubmenu,
            $snapshotMenu,
            $snapshotSubmenu,
            $snapshotSource,
        );
        return [$rawMenu, $rawSubmenu];
    }

    /**
     * Resolve raw menu/submenu for API consumers.
     *
     * **Parity with wp-admin:** The authoritative menu snapshot is persisted on real admin HTML requests
     * ({@see \FleekDash\Admin\AdminMenu::captureAdminMenuShutdown}). The SPA plugins grid triggers
     * {@code requestAdminMenuSnapshotRefresh} (hidden iframe) before GET /menu so the snapshot is fresh;
     * this merge path remains a **fallback** when the snapshot is stale/thin, the iframe is skipped, or
     * only REST is used.
     *
     * Merge rules and constants (core slugs, native parents, parity ratio): {@see MenuMergeStrategy}.
     *
     * @return array{0: array<int,mixed>, 1: array<string,mixed>, 2: string, 3: array<string, mixed>, 4: array<int,mixed>}
     */
    private function resolveRawMenuAndSubmenu(): array
    {
        // Fast path: a fresh per-user snapshot (just written by a real wp-admin request, e.g. the
        // hidden iframe in menu-wp-admin-snapshot-pipeline.ts) is the authoritative menu. Returning
        // it directly avoids running AdminMenuContextBuilder::ensureLoaded() in REST, which fires
        // admin_menu / _admin_menu / current_screen and can block for many seconds when plugins
        // perform remote HTTP calls in those hooks (root cause of /fleekdash/v1/menu timeouts).
        $fastPath = $this->tryFreshSnapshotFastPath();
        if ($fastPath !== null) {
            return $fastPath;
        }

        $this->adminMenuContextBuilder->ensureLoaded();
        global $menu, $submenu;

        $globalsMenu = \is_array($menu) ? $menu : [];
        $globalsSubmenu = \is_array($submenu) ? $submenu : [];

        $resolver = new \FleekDash\Admin\Menu\MenuSourceResolver($this->menuCapture);
        [$snapshotMenu, $snapshotSubmenu, $snapshotSource] = $resolver->resolve();
        $snapshotMenu = \is_array($snapshotMenu) ? $snapshotMenu : [];
        $snapshotSubmenu = \is_array($snapshotSubmenu) ? $snapshotSubmenu : [];

        return $this->menuMergeStrategy->mergeGlobalsWithSnapshot(
            $globalsMenu,
            $globalsSubmenu,
            $snapshotMenu,
            $snapshotSubmenu,
            $snapshotSource,
        );
    }

    /**
     * Attempt to serve the menu directly from a fresh per-user snapshot, bypassing the full
     * admin-context rebuild + merge pipeline.
     *
     * Disabled via the `fleekdash_menu_fast_path_enabled` filter (returning false) or when the
     * snapshot is stale / missing / too thin to safely represent the menu.
     *
     * @return array{0: array<int,mixed>, 1: array<string,mixed>, 2: string, 3: array<string, mixed>, 4: array<int,mixed>}|null
     */
    private function tryFreshSnapshotFastPath(): ?array
    {
        if (!\apply_filters('fleekdash_menu_fast_path_enabled', true)) {
            return null;
        }

        $resolver = new \FleekDash\Admin\Menu\MenuSourceResolver($this->menuCapture);
        [$snapMenu, $snapSubmenu, $snapSource] = $resolver->resolve(false);

        $snapMenu = \is_array($snapMenu) ? $snapMenu : [];
        $snapSubmenu = \is_array($snapSubmenu) ? $snapSubmenu : [];
        $menuCount = \count($snapMenu);
        $submenuRows = $this->countSubmenuRows($snapSubmenu);

        if (!$this->isFastPathEligible($snapSource, $menuCount, $submenuRows)) {
            return null;
        }

        $resolutionMeta = [
            'snapshot_resolver_source' => $snapSource,
            'fast_path' => true,
            'snapshot_menu_count' => $menuCount,
            'snapshot_submenu_rows' => $submenuRows,
            'globals_menu_count' => 0,
            'globals_submenu_rows' => 0,
            'merged_menu_count' => $menuCount,
            'merged_submenu_rows' => $submenuRows,
            'globals_tree_empty' => true,
            'snapshot_tree_empty' => false,
            'merged_tree_empty' => false,
            'all_or_nothing_empty_guard_expected' => true,
            'merge_strategy' => 'fast_path_fresh_snapshot_only',
            'top_level_merge_strategy' => 'fast_path_fresh_snapshot_only',
        ];

        return [$snapMenu, $snapSubmenu, 'snapshot_fresh_fast_path', $resolutionMeta, $snapMenu];
    }

    private function isFastPathEligible(string $snapshotSource, int $menuCount, int $submenuRows): bool
    {
        return $snapshotSource === 'snapshot_fresh'
            && $menuCount >= self::FAST_PATH_MIN_MENU_ITEMS
            && $submenuRows >= self::FAST_PATH_MIN_SUBMENU_ROWS;
    }

    /** @param array<string, mixed> $submenu */
    private function countSubmenuRows(array $submenu): int
    {
        $count = 0;
        foreach ($submenu as $rows) {
            if (\is_array($rows)) {
                $count += \count($rows);
            }
        }
        return $count;
    }

    /**
     * Get menu data by role
     *
     * @param \WP_REST_Request $request Request with 'role' parameter
     * @return \WP_REST_Response
     */
    public function getMenuByRole(WP_REST_Request $request): WP_REST_Response
    {
        try {
            $payload = $this->resolveRoleMenuPayload($request);
            return $this->menuResponse($payload['data'], true);
        } catch (\Throwable $e) {
            error_log('FleekDash MenuDataAPI getMenuByRole Error: ' . $e->getMessage());
            return $this->menuResponse([
                'items' => [],
                'menu_hash' => MenuPayloadHash::EMPTY,
                'debug' => ['error' => 'Failed to generate menu', 'message' => $e->getMessage()],
            ], true);
        }
    }

    public function getCapabilityImpact(WP_REST_Request $request): WP_REST_Response
    {
        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated'])) {
            return new WP_REST_Response([
                'success' => false,
                'message' => self::MSG_NOT_AUTHENTICATED,
                'fallback_data' => ['unique_ids' => [], 'items' => []],
            ], 200);
        }

        $capability = \sanitize_key((string) $request->get_param('capability'));
        if ($capability === '') {
            return new WP_REST_Response([
                'success' => false,
                'message' => 'Capability is required',
                'fallback_data' => ['unique_ids' => [], 'items' => []],
            ], 200);
        }

        $request->set_param('include_denied', '1');
        $payload = $this->resolveRoleMenuPayload($request);
        $items = isset($payload['data']['items']) && \is_array($payload['data']['items'])
            ? $payload['data']['items']
            : [];
        $impact = $this->menuCapabilityImpactService->findItemsForCapability($items, $capability);

        return new WP_REST_Response([
            'success' => true,
            'data' => $impact,
        ], 200);
    }

    /** @return array{data: array<string, mixed>} */
    private function resolveRoleMenuPayload(WP_REST_Request $request): array
    {
        $emptyAuth = ['items' => [], 'menu_hash' => MenuPayloadHash::EMPTY, 'debug' => ['error' => self::MSG_NOT_AUTHENTICATED]];

        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated'])) {
            return ['data' => $emptyAuth];
        }

        $current_user_id = (int) \get_current_user_id();
        if ($current_user_id <= 0) {
            return ['data' => $emptyAuth];
        }
        [$rawMenu, $rawSubmenu] = $this->resolveRawMenuAndSubmenuForRole();
        return $this->menuRolePayloadService->resolveRoleMenuPayload(
            $request,
            $current_user_id,
            $rawMenu,
            $rawSubmenu,
        );
    }

    /**
     * Refresh menu (clear in-memory capture and return fresh merged menu)
     *
     * Do NOT mark snapshots stale or persist a REST rebuild here. POST /menu/refresh is triggered
     * from the SPA after iframe lifecycle events; rebuilding/persisting in the same REST request
     * produced thin $submenu trees and overwrote good wp-admin snapshots. GET /menu already runs
     * AdminMenuContextBuilder::ensureLoaded() and merges globals + persisted snapshot per slug.
     */
    public function refreshMenu(?WP_REST_Request $request = null): WP_REST_Response
    {
        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated']) || !AuthMiddleware::validateRestNonceForMutation($request)) {
            return $this->menuResponse([
                'items' => [],
                'menu_hash' => MenuPayloadHash::EMPTY,
                'debug' => ['error' => self::MSG_NOT_AUTHENTICATED],
            ]);
        }

        $this->menuCapture->clearInMemoryCache();
        return $this->getMenu();
    }

    /**
     * Clear menu cache
     *
     * Invalidates snapshots AND clears in-memory cache.
     * This ensures menu updates immediately when cache is cleared.
     */
    public function clearMenuCache(?WP_REST_Request $request = null): WP_REST_Response
    {
        $auth = AuthMiddleware::check('admin');
        if (empty($auth['authenticated']) || !AuthMiddleware::validateRestNonceForMutation($request)) {
            return new WP_REST_Response([
                'success' => false,
                'message' => 'Access limited',
                'fallback_data' => ['cleared' => false],
            ], 200);
        }

        $this->menuCacheLifecycleService->invalidateSnapshots('clearMenuCache');

        // Clear in-memory cache
        $this->menuCapture->clearInMemoryCache();

        return new WP_REST_Response([
            'success' => true,
            'message' => 'Menu snapshots and in-memory cache cleared. Menu will rebuild on next request.',
            'data' => [
                'cleared' => true,
                'snapshots_invalidated' => true,
            ],
        ], 200);
    }

    /**
     * Invalidate menu snapshots (force fresh rebuild)
     *
     * This endpoint is called when:
     * - User clicks "Reload Menu" button
     * - WordPress iframe detects menu changes
     * - Any other manual menu refresh is needed
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function invalidateSnapshots(?WP_REST_Request $request = null): WP_REST_Response
    {
        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated']) || !AuthMiddleware::validateRestNonceForMutation($request)) {
            return new WP_REST_Response([
                'success' => false,
                'message' => self::MSG_NOT_AUTHENTICATED,
                'fallback_data' => ['invalidated' => false],
            ], 200);
        }

        $result = $this->menuCacheLifecycleService->executeSnapshotInvalidation();
        return new WP_REST_Response($result['payload'], 200);
    }


    /**
     * Get PHP menu (same as window.FleekDash.adminMenu)
     *
     * Returns the exact same data that is injected into window.FleekDash.adminMenu
     * in the admin context. Uses AdminMenu::getAdminMenuItems() directly.
     *
     * @return \WP_REST_Response
     */
    public function getPhpMenu(): WP_REST_Response
    {
        try {
            $auth = AuthMiddleware::check('authenticated');
            if (empty($auth['authenticated'])) {
                return $this->menuResponse([
                    'items' => [],
                    'menu_hash' => MenuPayloadHash::EMPTY,
                    'debug' => $this->menuPhpMenuService->buildAuthDebugInfo($auth),
                ]);
            }

            $php_menu_items = $this->menuPhpMenuService->getAdminMenuItems();

            return $this->menuResponse([
                'items' => $php_menu_items,
                'menu_hash' => MenuPayloadHash::hashNormalizedMenu($php_menu_items),
                'debug' => [
                    'source' => 'PHP Menu (window.FleekDash.adminMenu)',
                    'count' => count($php_menu_items),
                    'method' => 'AdminMenu::getAdminMenuItems()',
                ],
            ]);
        } catch (\Throwable $e) {
            error_log('FleekDash MenuDataAPI PHP Menu Error: ' . $e->getMessage());
            error_log('FleekDash MenuDataAPI PHP Menu Error (Stack): ' . $e->getTraceAsString());
            return $this->menuResponse([
                'items' => [],
                'menu_hash' => MenuPayloadHash::EMPTY,
                'debug' => [
                    'error' => 'Failed to generate PHP menu',
                    'message' => $e->getMessage(),
                    'file' => basename($e->getFile()) . ':' . $e->getLine(),
                ],
            ]);
        }
    }

    /**
     * Get cache status
     */
    public function getCacheStatus(): WP_REST_Response
    {
        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated'])) {
            return new WP_REST_Response([
                'success' => false,
                'message' => self::MSG_NOT_AUTHENTICATED,
                'fallback_data' => [
                    'snapshot' => null,
                ],
            ], 200);
        }

        return new WP_REST_Response($this->menuCacheLifecycleService->buildCacheStatusPayload(), 200);
    }

    /**
     * Mark menu cache as cleared (called by useMenuCacheInvalidation after clearing localStorage)
     * Updates fleekdash_last_cache_check so should_invalidate becomes false on next poll
     */
    public function markCacheCleared(?WP_REST_Request $request = null): WP_REST_Response
    {
        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated']) || !AuthMiddleware::validateRestNonceForMutation($request)) {
            return new WP_REST_Response(['success' => false, 'message' => self::MSG_NOT_AUTHENTICATED], 200);
        }

        $this->menuCacheLifecycleService->markCacheCleared();

        return new WP_REST_Response(['success' => true], 200);
    }

    /**
     * Diagnostic endpoint for debugging menu issues in production.
     * Returns detailed information about all menu data sources without modifying anything.
     */
    public function getMenuDiagnostic(): WP_REST_Response
    {
        $auth = AuthMiddleware::check('authenticated');
        if (empty($auth['authenticated'])) {
            return new WP_REST_Response(['success' => false, 'message' => self::MSG_NOT_AUTHENTICATED], 200);
        }

        $user_id = (int) \get_current_user_id();
        $diagnostic = $this->menuDiagnosticService->buildDiagnostic($user_id);

        return new WP_REST_Response(['success' => true, 'diagnostic' => $diagnostic], 200);
    }

    /**
     * @param array<int,mixed> $rawMenu
     * @return array<int,string>
     */
    private function extractRawTopLevelSlugs(array $rawMenu): array
    {
        $slugs = [];
        foreach ($rawMenu as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $slug = isset($item[2]) && \is_string($item[2]) ? $item[2] : '';
            if ($slug !== '') {
                $slugs[] = $slug;
            }
        }
        return \array_values(\array_unique($slugs));
    }

    /**
     * @param array<int,mixed> $rawMenu
     */
    private function countRawActionableTopLevelItems(array $rawMenu): int
    {
        $count = 0;
        foreach ($rawMenu as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $slug = isset($item[2]) && \is_string($item[2]) ? $item[2] : '';
            if ($slug !== '') {
                $count++;
            }
        }
        return $count;
    }

    /**
     * @param array<int,array<string,mixed>> $formattedItems
     * @return array<int,string>
     */
    private function extractFormattedSourceSlugs(array $formattedItems): array
    {
        $slugs = [];
        foreach ($formattedItems as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $slug = isset($item['source_slug']) && \is_string($item['source_slug'])
                ? $item['source_slug']
                : '';
            if ($slug !== '') {
                $slugs[] = $slug;
            }
        }
        return \array_values(\array_unique($slugs));
    }

    /**
     * @param array<int,string> $rawSlugs
     * @param array<int,string> $formattedSlugs
     * @return array<int,string>
     */
    private function calculateMissingSlugs(array $rawSlugs, array $formattedSlugs): array
    {
        $formattedSet = [];
        foreach ($formattedSlugs as $slug) {
            $formattedSet[$slug] = true;
        }
        $missing = [];
        foreach ($rawSlugs as $slug) {
            if (!isset($formattedSet[$slug])) {
                $missing[] = $slug;
            }
        }
        return $missing;
    }

    /**
     * @param array<int,array<string,mixed>> $items
     * @return array<int,string>
     */
    private function extractRestoredCoreSlugs(array $items): array
    {
        $slugs = [];
        foreach ($items as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $isRestored = isset($item['is_restored_core']) && $item['is_restored_core'] === true;
            if (!$isRestored) {
                continue;
            }
            $slug = isset($item['source_slug']) && \is_string($item['source_slug'])
                ? $item['source_slug']
                : '';
            if ($slug !== '') {
                $slugs[] = $slug;
            }
        }
        return \array_values(\array_unique($slugs));
    }

    /**
     * @param array<int,array<string,mixed>> $items
     * @return array<int,string>
     */
    private function extractRestoredPostTypeSlugs(array $items): array
    {
        $slugs = [];
        foreach ($items as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $isRestored = isset($item['is_restored_post_type']) && $item['is_restored_post_type'] === true;
            if (!$isRestored) {
                continue;
            }
            $slug = isset($item['source_slug']) && \is_string($item['source_slug'])
                ? $item['source_slug']
                : '';
            if ($slug !== '') {
                $slugs[] = $slug;
            }
        }
        return \array_values(\array_unique($slugs));
    }

    /**
     * @param array<int,array<string,mixed>> $items
     */
    private function countIntegrationTopLevelItems(array $items): int
    {
        $count = 0;
        foreach ($items as $item) {
            if (!\is_array($item)) {
                continue;
            }
            $isIntegration = isset($item['integration_type']) && \is_string($item['integration_type']);
            if ($isIntegration) {
                $count++;
            }
        }
        return $count;
    }

    /**
     * @param array<string,mixed> $rawSubmenu
     */
    private function countRawDuplicateSubmenuEntries(array $rawSubmenu): int
    {
        $duplicates = 0;
        foreach ($rawSubmenu as $submenuItems) {
            if (!\is_array($submenuItems)) {
                continue;
            }
            $duplicates += $this->countDuplicateKeysInSubmenu($submenuItems);
        }
        return $duplicates;
    }

    /**
     * @param array<int,mixed> $submenuItems
     */
    private function countDuplicateKeysInSubmenu(array $submenuItems): int
    {
        $seen = [];
        $duplicates = 0;
        foreach ($submenuItems as $subItem) {
            $key = $this->extractRawSubmenuDedupeKey($subItem);
            if ($key === '') {
                continue;
            }
            if (isset($seen[$key])) {
                $duplicates++;
                continue;
            }
            $seen[$key] = true;
        }
        return $duplicates;
    }

    private function extractRawSubmenuDedupeKey(mixed $subItem): string
    {
        if (!\is_array($subItem)) {
            return '';
        }
        $slug = isset($subItem[2]) && \is_string($subItem[2]) ? $subItem[2] : '';
        if ($slug !== '') {
            return $slug;
        }
        $name = isset($subItem[0]) && \is_string($subItem[0]) ? \trim(\wp_strip_all_tags($subItem[0])) : '';
        return $name !== '' ? $name : '';
    }

    private function resetLegacyOverridesOnce(): void
    {
        $alreadyReset = (int) \get_option(self::OVERRIDES_RESET_FLAG, 0) === 1;
        if ($alreadyReset) {
            return;
        }
        $this->menuOverridesService->resetAllRoleOverrides();
        \update_option(self::OVERRIDES_RESET_FLAG, 1, false);
    }

    /**
     * ⚠️ DEPRECATED - Toolbar methods removed
     *
     * All toolbar functionality moved to ToolbarDataAPI
     * See: includes/API/ToolbarDataAPI.php
     */

    private function menuResponse(array $payload, bool $includeMeta = false): WP_REST_Response
    {
        // Ensure items is always an array
        $items = isset($payload['items']) && is_array($payload['items'])
            ? $payload['items']
            : [];

        // Ensure menu_hash exists
        $menu_hash = $payload['menu_hash'] ?? MenuPayloadHash::hashNormalizedMenu($items);

        $data = [
            'items' => $items,
            'timestamp' => time(),
            'source' => 'MenuDataAPI',
            'menu_hash' => $menu_hash,
            'debug' => $payload['debug'] ?? []
        ];

        if ($includeMeta && !empty($payload['meta'])) {
            foreach (['role', 'user_id', 'capabilities'] as $key) {
                if (isset($payload['meta'][$key])) {
                    $data[$key] = $payload['meta'][$key];
                }
            }
        }

        $response = new WP_REST_Response($data, 200);
        $response->header('Cache-Control', 'no-cache, no-store, must-revalidate, private');
        $response->header('Pragma', 'no-cache');
        $response->header('Expires', '0');

        return $response;
    }
}
