<?php
/**
 * Menu Change Notifier
 *
 * Detects WordPress menu changes and notifies React app via postMessage.
 * Uses existing MessageBus system ('forceMenuApiRefresh' event).
 *
 * This component eliminates the need for:
 * - Menu Pixel iframe (338 lines)
 * - iframe Monitor MutationObserver (791 lines)
 * - Complex DOM parsing and postMessage handling
 *
 * Plugin/theme lifecycle hooks notify immediately. CPT/taxonomy changes are detected by comparing a
 * fingerprint (active plugins, theme, custom post types, taxonomies) once per admin_footer so iframe
 * navigations do not trigger a refresh on every request.
 *
 * @package FleekDash\Admin
 * @since V2.2
 * @version 1.0.0
 */

namespace FleekDash\Admin;

class MenuChangeNotifier
{
    private const MENU_STRUCTURE_FP_OPTION = 'fleekdash_admin_menu_structure_fp';

    /**
     * Track if notification was already sent this request
     *
     * @var bool
     */
    private static $notificationSent = false;

    /**
     * True when plugin/theme lifecycle already queued a footer notify this request.
     * Used to avoid double postMessage when fingerprint also detects a change.
     *
     * @var bool
     */
    private static $lifecycleMenuNotifyQueued = false;

    /**
     * True after {@see notifyMenuChanged()} scheduled the admin_footer callback this request.
     * Prevents multiple admin_footer closures when notifyMenuChanged runs more than once before footer.
     *
     * @var bool
     */
    private static $footerMenuNotifyScheduled = false;

    /**
     * Constructor - Register WordPress hooks
     */
    public function __construct()
    {
        // Track plugin changes
        \add_action('activated_plugin', [$this, 'onPluginActivated'], 10, 1);
        \add_action('deactivated_plugin', [$this, 'onPluginDeactivated'], 10, 1);
        \add_action('upgrader_process_complete', [$this, 'onPluginUpgraded'], 10, 2);
        \add_action('deleted_plugin', [$this, 'onPluginDeleted'], 10, 2);
        \add_action('switch_theme', [$this, 'onThemeSwitched'], 10, 2);

        // CPT/taxonomy: registered_post_type / registered_taxonomy run on every admin load — do not notify there.
        // Instead compare a structure fingerprint on admin_footer (see maybeNotifyMenuStructureFingerprintChange).
        \add_action('admin_footer', [$this, 'maybeNotifyMenuStructureFingerprintChange'], 998);
    }

    /**
     * Invalidate all menu and toolbar snapshots (for all users).
     * Call this when plugins/theme/CPT change so frontend gets fresh menu on next fetch.
     *
     * Can be called statically from REST endpoints (e.g., FleekDocsSettingsAPI).
     */
    public static function invalidateMenuAndToolbarSnapshots(): void
    {
        try {
            if (\class_exists(\FleekDash\Admin\Menu\MenuSnapshotUserMeta::class)) {
                \FleekDash\Admin\Menu\MenuSnapshotUserMeta::markAllStale();
            }
            if (\class_exists(\FleekDash\Admin\Menu\ToolbarCaptureService::class)) {
                \FleekDash\Admin\Menu\ToolbarCaptureService::deleteAllSnapshots();
            }
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[FleekDash MenuChangeNotifier] Failed to invalidate snapshots: ' . $e->getMessage());
            }
        }
    }

    /**
     * Handle plugin activation
     *
     * @param string $plugin Plugin basename (e.g., 'akismet/akismet.php')
     */
    public function onPluginActivated($plugin)
    {
        if (\class_exists('\FleekDash\Integration\PluginLifecycleSync')) {
            \FleekDash\Integration\PluginLifecycleSync::syncRegistryFromPluginLifecycle((string) $plugin, true);
        }

        self::invalidateMenuAndToolbarSnapshots();
        $this->attemptRecaptureSnapshotForCurrentUser();

        // Ensure update caches + frontend invalidation flag are consistent even in REST contexts
        $this->invalidateWordpressPluginCachesBestEffort();

        self::$lifecycleMenuNotifyQueued = true;
        $this->notifyMenuChanged('plugin_activated', [
            'plugin' => $plugin,
            'plugin_name' => $this->getPluginName($plugin)
        ]);
    }

    /**
     * Handle plugin deactivation
     *
     * @param string $plugin Plugin basename
     */
    public function onPluginDeactivated($plugin)
    {
        if (\class_exists('\FleekDash\Integration\PluginLifecycleSync')) {
            \FleekDash\Integration\PluginLifecycleSync::syncRegistryFromPluginLifecycle((string) $plugin, false);
        }

        self::invalidateMenuAndToolbarSnapshots();
        $this->attemptRecaptureSnapshotForCurrentUser();

        // Ensure update caches + frontend invalidation flag are consistent even in REST contexts
        $this->invalidateWordpressPluginCachesBestEffort();

        self::$lifecycleMenuNotifyQueued = true;
        $this->notifyMenuChanged('plugin_deactivated', [
            'plugin' => $plugin,
            'plugin_name' => $this->getPluginName($plugin)
        ]);
    }

    /**
     * @param mixed $_upgrader {@see \WP_Upgrader} from core (unused)
     * @param mixed $hook_extra Second argument from {@see 'upgrader_process_complete'}
     */
    public function onPluginUpgraded(mixed $_upgrader = null, mixed $hook_extra = null): void // NOSONAR php:S1172 — hook passes WP_Upgrader as first argument
    {
        $hook_extra = \is_array($hook_extra) ? $hook_extra : [];
        if (empty($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
            return;
        }
        if (empty($hook_extra['action']) || !\in_array($hook_extra['action'], ['update', 'install'], true)) {
            return;
        }

        $plugins = $hook_extra['plugins'] ?? [];
        $plugin = $hook_extra['plugin'] ?? 'unknown';

        // ROOT FIX: After an upgrade, WordPress update counts can become stale/inconsistent
        // unless site transients are refreshed. Do this once here (upgrade is already a heavy op).
        self::invalidateMenuAndToolbarSnapshots();
        $this->attemptRecaptureSnapshotForCurrentUser();

        $this->refreshWordpressUpdateCachesBestEffort();
        $this->invalidateWordpressPluginCachesBestEffort();

        self::$lifecycleMenuNotifyQueued = true;
        $this->notifyMenuChanged('plugin_upgraded', [
            'action' => $hook_extra['action'],
            'plugins' => \is_array($plugins) ? $plugins : [],
            'plugin' => \is_string($plugin) ? $plugin : 'unknown',
        ]);
    }

    public function onPluginDeleted(string $plugin_file, bool $deleted): void
    {
        if (!$deleted) {
            return;
        }

        self::invalidateMenuAndToolbarSnapshots();
        $this->attemptRecaptureSnapshotForCurrentUser();

        $this->invalidateWordpressPluginCachesBestEffort();

        self::$lifecycleMenuNotifyQueued = true;
        $this->notifyMenuChanged('plugin_deleted', [
            'plugin' => $plugin_file,
            'plugin_name' => $this->getPluginName($plugin_file),
        ]);
    }

    /**
     * @param mixed $new_name Stylesheet or theme name from {@see 'switch_theme'}
     * @param mixed $_new_theme {@see \WP_Theme} from core (unused)
     */
    public function onThemeSwitched(mixed $new_name = null, mixed $_new_theme = null): void // NOSONAR php:S1172 — hook passes WP_Theme as second argument
    {
        $new_name = $new_name ?? '';

        self::invalidateMenuAndToolbarSnapshots();
        $this->attemptRecaptureSnapshotForCurrentUser();

        // Theme switch can affect admin menu entries (and sometimes update cache behaviour)
        $this->invalidateWordpressPluginCachesBestEffort();

        self::$lifecycleMenuNotifyQueued = true;
        $this->notifyMenuChanged('theme_switched', [
            'theme' => \is_string($new_name) ? $new_name : 'unknown',
        ]);
    }

    /**
     * Best-effort: invalidate plugin/theme related caches and set frontend invalidation flag.
     *
     * This MUST work in REST requests (React plugin grid) where admin_footer never runs,
     * so the invalidation flag is the reliable trigger that the React app already polls for.
     *
     * @return void
     */
    private function invalidateWordpressPluginCachesBestEffort(): void
    {
        try {
            if (\class_exists('\FleekDash\API\Plugins\PluginHelpers')) {
                \FleekDash\API\Plugins\PluginHelpers::invalidatePluginsCache();
                return;
            }
        } catch (\Throwable $e) {
            // Continue to fallback
        }

        try {
            // Fallback: minimal invalidation + frontend flag
            \update_option('fleekdash_invalidate_frontend_cache', time(), false);

            // Do NOT delete update transients here!
            // delete_site_transient('update_plugins') etc. destroys the update counts,
            // causing wp_get_update_data() to return 0 until WordPress cron repopulates them.
            // The plugin cache (list of plugins) is separate from update counts.
            // We only need to invalidate FleekDash's internal caches, not WordPress's update transients.

            // Only flush object cache (if available) - this doesn't affect update transients
            if (\function_exists('wp_cache_flush')) {
                \wp_cache_flush();
            }
            \do_action('fleekdash_plugins_cache_invalidated');
        } catch (\Throwable $e) {
            // Best-effort only
        }
    }

    /**
     * Best-effort: refresh WP update transients to keep update counters consistent after upgrades.
     *
     * @return void
     */
    private function refreshWordpressUpdateCachesBestEffort(): void
    {
        try {
            if (!\function_exists('wp_update_plugins')) {
                require_once ABSPATH . 'wp-admin/includes/update.php';
            }

            // Do NOT call wp_clean_update_cache() here!
            // Cleaning the cache before wp_update_plugins() causes a race condition:
            // - Cache is cleared (update counts = 0)
            // - wp_update_plugins() starts HTTP request to wordpress.org (slow)
            // - Menu API is called before HTTP completes → returns 0 counts
            // Instead, let wp_update_plugins() naturally update the transients.

            if (\function_exists('wp_update_plugins')) {
                \wp_update_plugins();
            }
        } catch (\Throwable $e) {
            // Best-effort only
        }
    }

    /**
     * Best-effort recapture after lifecycle changes during a full admin request (non-REST).
     * REST activate/deactivate/update/delete use PluginsController::syncMenuSnapshotAfterPluginLifecycleChange()
     * so snapshots stay consistent when the SPA mutates plugins.
     */
    private function attemptRecaptureSnapshotForCurrentUser(): void
    {
        if (\defined('REST_REQUEST') && REST_REQUEST) {
            return;
        }
        $userId = (int) \get_current_user_id();
        if ($userId <= 0) {
            return;
        }

        try {
            $contextBuilder = new \FleekDash\Admin\Menu\AdminMenuContextBuilder();
            $contextBuilder->ensureLoaded();
            $capture = new \FleekDash\Admin\Menu\MenuCaptureService();
            $capture->clearInMemoryCache();
            $capture->ensureMenuCaptured();
            $capture->persistSnapshotForCurrentUser();
        } catch (\Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[FleekDash MenuChangeNotifier] Snapshot recapture failed: ' . $e->getMessage());
            }
        }
    }

    /**
     * Hash of everything that typically changes the admin menu (plugins, theme, CPTs, taxonomies).
     * Compared once per admin request so iframe navigations do not spam the parent.
     *
     * @return string
     */
    private function computeAdminMenuStructureFingerprint()
    {
        $active = (array) \get_option('active_plugins', []);
        if (\is_multisite()) {
            $network = (array) \get_site_option('active_sitewide_plugins', []);
            $active = \array_merge($active, \array_keys($network));
        }
        \sort($active, SORT_STRING);

        $cpts = \get_post_types(['_builtin' => false], 'names');
        $cpts = \is_array($cpts) ? $cpts : [];
        \sort($cpts, SORT_STRING);

        $tax = \get_taxonomies(['_builtin' => false], 'names');
        $tax = \is_array($tax) ? $tax : [];
        \sort($tax, SORT_STRING);

        $parts = [
            'plugins:' . \implode(',', $active),
            'stylesheet:' . (string) \get_option('stylesheet', ''),
            'template:' . (string) \get_option('template', ''),
            'cpt:' . \implode(',', $cpts),
            'tax:' . \implode(',', $tax),
        ];

        return \hash('sha256', \implode('|', $parts));
    }

    /**
     * After admin bootstrap, if menu-affecting structure changed (e.g. new CPT via ACF) notify parent once.
     * Skips when plugin/theme lifecycle already scheduled a notify this request.
     *
     * @return void
     */
    public function maybeNotifyMenuStructureFingerprintChange()
    {
        $canEvaluateFingerprint =
            !(\defined('REST_REQUEST') && REST_REQUEST) && !self::$notificationSent;
        if (!$canEvaluateFingerprint) {
            return;
        }

        $current = $this->computeAdminMenuStructureFingerprint();
        $stored = (string) \get_option(self::MENU_STRUCTURE_FP_OPTION, '');

        if ($current === $stored) {
            return;
        }

        \update_option(self::MENU_STRUCTURE_FP_OPTION, $current, false);

        // First admin request after install: baseline only. Skip if lifecycle already scheduled notify.
        $shouldNotify = $stored !== '' && !self::$lifecycleMenuNotifyQueued;
        if (!$shouldNotify) {
            return;
        }

        self::invalidateMenuAndToolbarSnapshots();
        $this->attemptRecaptureSnapshotForCurrentUser();

        $this->notifyMenuChanged('menu_structure_changed', [
            'fingerprint_changed' => true,
        ]);
    }

    /**
     * Notify React app that menu changed
     *
     * Injects inline script that sends postMessage to parent window.
     * The JavaScript will only send if we're inside an iframe (checked at runtime).
     * This allows the notification to work in both iframe and non-iframe contexts,
     * but only actually sends when inside FleekDash iframe.
     *
     * @param string $reason Change reason (e.g., 'plugin_activated')
     * @param array  $context Additional context data
     */
    private function notifyMenuChanged($reason, $context = [])
    {
        // Prevent duplicate notifications in same request
        // This is sufficient since events are rare and don't cause loops
        if (self::$notificationSent) {
            return;
        }

        if (self::$footerMenuNotifyScheduled) {
            return;
        }
        self::$footerMenuNotifyScheduled = true;

        // Inject script on admin_footer (runs once at end of page)
        \add_action('admin_footer', function () use ($reason, $context) {
            // Double-check to prevent duplicates
            if (self::$notificationSent) {
                return;
            }
            self::$notificationSent = true;

            // Snapshot invalidation already done synchronously in handler (invalidateMenuAndToolbarSnapshots)
            // before this admin_footer runs. Just send the postMessage to trigger frontend refresh.

            $payload = [
                'reason' => $reason,
                'timestamp' => time(),
                'context' => $context,
                // Parent must GET /menu only — never POST /menu/refresh from iframe lifecycle (thin REST rebuild).
                'skipServerSnapshotRebuild' => true,
            ];
            ?>
            <script>
            (function() {
                // Only send if we're in an iframe
                if (window.parent && window.parent !== window) {
                    try {
                        // Use existing MessageBus event type!
                        // Type 'forceMenuApiRefresh' is already defined in:
                        // src/__wordpress-bridge__/messaging/message-types.ts
                        window.parent.postMessage({
                            type: 'forceMenuApiRefresh',
                            payload: <?php echo json_encode($payload); ?>,
                            source: 'MenuChangeNotifier',
                            version: '1.0.0'
                        }, '*');

                        console.log(
                            '%c🔔 [MenuChangeNotifier] Menu change notified',
                            'color: #10b981; font-weight: bold;',
                            <?php echo json_encode($payload); ?>
                        );
                    } catch (error) {
                        console.error('[MenuChangeNotifier] postMessage failed:', error);
                    }
                }
            })();
            </script>
            <?php
        }, 999); // Late priority to ensure it runs after other scripts
    }

    /**
     * Get plugin name from basename
     *
     * @param string $plugin_basename Plugin basename (e.g., 'akismet/akismet.php')
     * @return string Plugin name
     */
    private function getPluginName($plugin_basename)
    {
        if (!\function_exists('get_plugins')) {
            \FleekDash\Core\PluginHelper::ensurePluginApiLoaded();
        }

        $all_plugins = \get_plugins();

        if (isset($all_plugins[$plugin_basename])) {
            return $all_plugins[$plugin_basename]['Name'];
        }

        return $plugin_basename;
    }
}
