<?php
/**
 * Name: Cleanup Removed Plugins
 * Description: Find and remove database leftovers from uninstalled plugins and themes - orphaned tables, options, cron jobs, and metadata zombies that waste space and slow down your site
 * Category: Optimization
 * Icon: -
 * UseCases: [
 *   {
 *     "title": "Orphaned Table Detection",
 *     "description": "Identify database tables created by plugins that no longer exist, freeing up significant database space."
 *   },
 *   {
 *     "title": "Leftover Options Cleanup",
 *     "description": "Find and remove wp_options entries from plugins that were deleted without proper uninstall routines."
 *   },
 *   {
 *     "title": "Zombie Cron Jobs",
 *     "description": "Detect and remove scheduled tasks from plugins that are no longer active, preventing unnecessary server load."
 *   },
 *   {
 *     "title": "Database Archaeology",
 *     "description": "Explore your database to find remnants of plugins you forgot you ever installed, with safe cleanup options."
 *   }
 * ]
 * Added: 2026-02-06
 * Tags: cleanup, orphaned, database, tables, options, cron, plugins, themes, leftovers, zombies, uninstall
 * IsBeta: true
 * WhereToFind: {
 *   "location": "Switchboard Menu",
 *   "url": "admin.php?page=switchboard-cleanup-removed-plugins",
 *   "instructions": "Access the full Cleanup Removed Plugins dashboard from Switchboard > Cleanup Removed Plugins in the admin menu.",
 *   "newTab": false
 * }
 */

// Prevent direct access
if (!defined('ABSPATH')) exit;

class SwitchboardCleanupRemovedPlugins {

    /**
     * Option key for storing settings
     */
    private $option_key = 'switchboard_cleanup-removed-plugins_settings';

    /**
     * Known plugin prefixes for detection
     * Maps plugin slug/name to their common database prefixes
     */
    private $known_plugin_prefixes = [];

    /**
     * Cached all plugin prefixes (memoization)
     */
    private $cached_all_prefixes = null;

    /**
     * WordPress core tables that should never be touched
     */
    private $core_tables = [
        'commentmeta', 'comments', 'links', 'options', 'postmeta',
        'posts', 'term_relationships', 'term_taxonomy', 'termmeta',
        'terms', 'usermeta', 'users'
    ];

    /**
     * Core WordPress option prefixes that should be protected
     */
    private $protected_option_prefixes = [
        'siteurl', 'home', 'blogname', 'blogdescription', 'admin_email',
        'users_can_register', 'default_role', 'date_format', 'time_format',
        'timezone_string', 'permalink_structure', 'template', 'stylesheet',
        'active_plugins', 'current_theme', 'cron', 'db_version', 'sidebars_widgets',
        'widget_', 'theme_mods_', 'nav_menu_', 'rewrite_rules', 'auth_key',
        'auth_salt', 'logged_in_key', 'logged_in_salt', 'nonce_key', 'nonce_salt',
        'secure_auth_key', 'secure_auth_salt', '_transient_', '_site_transient_',
        'user_roles', 'fresh_site', 'WPLANG', 'wp_user_roles', 'can_compress_scripts',
        'page_on_front', 'page_for_posts', 'show_on_front', 'posts_per_page',
        'category_base', 'tag_base', 'upload_path', 'upload_url_path',
        'thumbnail_size_w', 'thumbnail_size_h', 'medium_size_w', 'medium_size_h',
        'large_size_w', 'large_size_h', 'image_default_', 'blog_public',
        'default_comment_status', 'default_ping_status', 'comment_moderation',
        'comments_notify', 'moderation_notify', 'comment_registration',
        'close_comments_for_old_posts', 'thread_comments', 'page_comments',
        'switchboard_', // Protect our own options
    ];

    /**
     * Constructor - register hooks and initialize
     */
    public function __construct() {
        $this->init_known_prefixes();

        // Admin menu
        add_action('admin_menu', [$this, 'register_admin_page'], 99);

        // Enqueue assets
        add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);

        // AJAX handlers
        add_action('wp_ajax_switchboard_crp_scan', [$this, 'ajax_scan']);
        add_action('wp_ajax_switchboard_crp_delete_table', [$this, 'ajax_delete_table']);
        add_action('wp_ajax_switchboard_crp_delete_options', [$this, 'ajax_delete_options']);
        add_action('wp_ajax_switchboard_crp_delete_cron', [$this, 'ajax_delete_cron']);
        add_action('wp_ajax_switchboard_crp_bulk_cleanup', [$this, 'ajax_bulk_cleanup']);
        add_action('wp_ajax_switchboard_crp_save_settings', [$this, 'ajax_save_settings']);
        add_action('wp_ajax_switchboard_crp_get_option_value', [$this, 'ajax_get_option_value']);
        add_action('wp_ajax_switchboard_crp_link_prefix', [$this, 'ajax_link_prefix']);
        add_action('wp_ajax_switchboard_crp_get_active_plugins', [$this, 'ajax_get_active_plugins']);
        add_action('wp_ajax_switchboard_crp_get_ignored', [$this, 'ajax_get_ignored']);
        add_action('wp_ajax_switchboard_crp_unignore', [$this, 'ajax_unignore']);

        // Register with data registry
        $this->register_with_data_registry();
    }

    /**
     * Initialize known plugin prefixes for better detection
     */
    private function init_known_prefixes() {
        // Popular plugins and their known database prefixes
        // Format: 'table_prefix' => ['plugin_file' => 'path/file.php', 'name' => 'Plugin Name']
        $this->known_plugin_prefixes = [
            // Switchboard Pro (this plugin - always active when this code runs)
            'switchboard' => ['plugin_file' => 'wpswitchboard-pro/switchboard.php', 'name' => 'Switchboard Pro'],
            'wpsb' => ['plugin_file' => 'wpswitchboard-pro/switchboard.php', 'name' => 'Switchboard Pro'],

            // WooCommerce
            'woocommerce' => ['plugin_file' => 'woocommerce/woocommerce.php', 'name' => 'WooCommerce'],
            'wc_' => ['plugin_file' => 'woocommerce/woocommerce.php', 'name' => 'WooCommerce'],

            // Yoast SEO
            'yoast' => ['plugin_file' => 'wordpress-seo/wp-seo.php', 'name' => 'Yoast SEO'],
            'wpseo' => ['plugin_file' => 'wordpress-seo/wp-seo.php', 'name' => 'Yoast SEO'],

            // Elementor
            'elementor' => ['plugin_file' => 'elementor/elementor.php', 'name' => 'Elementor'],

            // Contact Form 7
            'wpcf7' => ['plugin_file' => 'contact-form-7/wp-contact-form-7.php', 'name' => 'Contact Form 7'],

            // Wordfence
            'wordfence' => ['plugin_file' => 'wordfence/wordfence.php', 'name' => 'Wordfence'],
            'wf' => ['plugin_file' => 'wordfence/wordfence.php', 'name' => 'Wordfence'],

            // WP Rocket
            'wp_rocket' => ['plugin_file' => 'wp-rocket/wp-rocket.php', 'name' => 'WP Rocket'],
            'rocket_' => ['plugin_file' => 'wp-rocket/wp-rocket.php', 'name' => 'WP Rocket'],

            // Jetpack
            'jetpack' => ['plugin_file' => 'jetpack/jetpack.php', 'name' => 'Jetpack'],

            // All in One SEO
            'aioseo' => ['plugin_file' => 'all-in-one-seo-pack/all_in_one_seo_pack.php', 'name' => 'All in One SEO'],

            // Rank Math
            'rank_math' => ['plugin_file' => 'seo-by-rank-math/rank-math.php', 'name' => 'Rank Math'],
            'rankmath' => ['plugin_file' => 'seo-by-rank-math/rank-math.php', 'name' => 'Rank Math'],

            // ACF
            'acf' => ['plugin_file' => 'advanced-custom-fields/acf.php', 'name' => 'Advanced Custom Fields'],

            // WP Forms
            'wpforms' => ['plugin_file' => 'wpforms-lite/wpforms.php', 'name' => 'WPForms'],

            // Gravity Forms
            'gf_' => ['plugin_file' => 'gravityforms/gravityforms.php', 'name' => 'Gravity Forms'],
            'gravityforms' => ['plugin_file' => 'gravityforms/gravityforms.php', 'name' => 'Gravity Forms'],
            'rg_' => ['plugin_file' => 'gravityforms/gravityforms.php', 'name' => 'Gravity Forms'],

            // Redirection
            'redirection' => ['plugin_file' => 'redirection/redirection.php', 'name' => 'Redirection'],

            // UpdraftPlus
            'updraft' => ['plugin_file' => 'updraftplus/updraftplus.php', 'name' => 'UpdraftPlus'],

            // TablePress
            'tablepress' => ['plugin_file' => 'tablepress/tablepress.php', 'name' => 'TablePress'],

            // WPML
            'wpml' => ['plugin_file' => 'sitepress-multilingual-cms/sitepress.php', 'name' => 'WPML'],
            'icl_' => ['plugin_file' => 'sitepress-multilingual-cms/sitepress.php', 'name' => 'WPML'],

            // MonsterInsights
            'monsterinsights' => ['plugin_file' => 'google-analytics-for-wordpress/googleanalytics.php', 'name' => 'MonsterInsights'],

            // LiteSpeed Cache
            'litespeed' => ['plugin_file' => 'litespeed-cache/litespeed-cache.php', 'name' => 'LiteSpeed Cache'],

            // Smush
            'smush' => ['plugin_file' => 'wp-smushit/wp-smush.php', 'name' => 'Smush'],

            // Really Simple SSL
            'rsssl' => ['plugin_file' => 'really-simple-ssl/rlrsssl-really-simple-ssl.php', 'name' => 'Really Simple SSL'],

            // All-in-One WP Migration
            'ai1wm' => ['plugin_file' => 'all-in-one-wp-migration/all-in-one-wp-migration.php', 'name' => 'All-in-One WP Migration'],

            // BuddyPress
            'bp_' => ['plugin_file' => 'buddypress/bp-loader.php', 'name' => 'BuddyPress'],
            'buddypress' => ['plugin_file' => 'buddypress/bp-loader.php', 'name' => 'BuddyPress'],

            // bbPress
            'bbp_' => ['plugin_file' => 'bbpress/bbpress.php', 'name' => 'bbPress'],
            'bbpress' => ['plugin_file' => 'bbpress/bbpress.php', 'name' => 'bbPress'],

            // Easy Digital Downloads
            'edd_' => ['plugin_file' => 'easy-digital-downloads/easy-digital-downloads.php', 'name' => 'Easy Digital Downloads'],

            // MemberPress
            'mepr' => ['plugin_file' => 'memberpress/memberpress.php', 'name' => 'MemberPress'],

            // WP Mail SMTP
            'wp_mail_smtp' => ['plugin_file' => 'wp-mail-smtp/wp_mail_smtp.php', 'name' => 'WP Mail SMTP'],

            // Broken Link Checker
            'blc' => ['plugin_file' => 'broken-link-checker/broken-link-checker.php', 'name' => 'Broken Link Checker'],

            // OptinMonster
            'optin_monster' => ['plugin_file' => 'optinmonster/optin-monster-wp-api.php', 'name' => 'OptinMonster'],
            'om_' => ['plugin_file' => 'optinmonster/optin-monster-wp-api.php', 'name' => 'OptinMonster'],

            // Sucuri
            'sucuri' => ['plugin_file' => 'sucuri-scanner/sucuri.php', 'name' => 'Sucuri Security'],

            // W3 Total Cache
            'w3tc' => ['plugin_file' => 'w3-total-cache/w3-total-cache.php', 'name' => 'W3 Total Cache'],

            // WP Super Cache
            'wp_cache' => ['plugin_file' => 'wp-super-cache/wp-cache.php', 'name' => 'WP Super Cache'],

            // Autoptimize
            'autoptimize' => ['plugin_file' => 'autoptimize/autoptimize.php', 'name' => 'Autoptimize'],

            // The Events Calendar
            'tribe_' => ['plugin_file' => 'the-events-calendar/the-events-calendar.php', 'name' => 'The Events Calendar'],

            // LearnDash
            'learndash' => ['plugin_file' => 'sfwd-lms/sfwd_lms.php', 'name' => 'LearnDash'],

            // LifterLMS
            'lifterlms' => ['plugin_file' => 'lifterlms/lifterlms.php', 'name' => 'LifterLMS'],
            'llms_' => ['plugin_file' => 'lifterlms/lifterlms.php', 'name' => 'LifterLMS'],

            // SeedProd
            'seedprod' => ['plugin_file' => 'coming-soon/coming-soon.php', 'name' => 'SeedProd'],

            // Duplicator
            'duplicator' => ['plugin_file' => 'duplicator/duplicator.php', 'name' => 'Duplicator'],

            // ActionScheduler (used by WooCommerce and others)
            'actionscheduler' => ['plugin_file' => null, 'name' => 'Action Scheduler'],

            // Query Monitor
            'qm_' => ['plugin_file' => 'query-monitor/query-monitor.php', 'name' => 'Query Monitor'],

            // Katorymnd Reaction Process Plugin (from the article)
            'katorymnd' => ['plugin_file' => null, 'name' => 'Katorymnd Reaction'],

            // wpForo Forum
            'wpforo' => ['plugin_file' => 'wpforo/wpforo.php', 'name' => 'wpForo Forum'],

            // Fluent Forms
            'fluentform' => ['plugin_file' => 'fluentform/fluentform.php', 'name' => 'Fluent Forms'],
            'ff_' => ['plugin_file' => 'fluentform/fluentform.php', 'name' => 'Fluent Forms'],

            // Fluent CRM
            'fluentcrm' => ['plugin_file' => 'fluent-crm/fluent-crm.php', 'name' => 'FluentCRM'],
            'fc_' => ['plugin_file' => 'fluent-crm/fluent-crm.php', 'name' => 'FluentCRM'],

            // WP Statistics
            'statistics' => ['plugin_file' => 'wp-statistics/wp-statistics.php', 'name' => 'WP Statistics'],

            // WooCommerce Subscriptions
            'wcs_' => ['plugin_file' => 'woocommerce-subscriptions/woocommerce-subscriptions.php', 'name' => 'WooCommerce Subscriptions'],

            // WP Mail Logging
            'wp_mail_logging' => ['plugin_file' => 'wp-mail-logging/wp-mail-logging.php', 'name' => 'WP Mail Logging'],

            // Formidable Forms
            'frm_' => ['plugin_file' => 'formidable/formidable.php', 'name' => 'Formidable Forms'],
            'formidable' => ['plugin_file' => 'formidable/formidable.php', 'name' => 'Formidable Forms'],

            // WP Activity Log
            'wsal_' => ['plugin_file' => 'wp-security-audit-log/wp-security-audit-log.php', 'name' => 'WP Activity Log'],

            // Simple History
            'simple_history' => ['plugin_file' => 'simple-history/simple-history.php', 'name' => 'Simple History'],
        ];
    }

    /**
     * Get dynamic prefixes extracted from active plugins
     * This supplements the known_plugin_prefixes with automatically detected prefixes
     */
    private function get_dynamic_plugin_prefixes() {
        $dynamic_prefixes = [];
        $active_plugins = $this->get_active_plugins();

        foreach ($active_plugins as $plugin_file) {
            // Extract folder name (e.g., 'wpswitchboard-pro' from 'wpswitchboard-pro/switchboard.php')
            $folder = dirname($plugin_file);
            if ($folder === '.') {
                continue;
            }

            // Get plugin data for display name
            $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file, false, false);
            $plugin_name = !empty($plugin_data['Name']) ? $plugin_data['Name'] : ucwords(str_replace(['-', '_'], ' ', $folder));

            // Generate possible prefixes from folder name
            $prefixes = $this->extract_prefixes_from_name($folder);

            foreach ($prefixes as $prefix) {
                // Skip if already in known prefixes
                if (isset($this->known_plugin_prefixes[$prefix])) {
                    continue;
                }
                $dynamic_prefixes[$prefix] = [
                    'plugin_file' => $plugin_file,
                    'name' => $plugin_name,
                    'dynamic' => true,
                ];
            }
        }

        return $dynamic_prefixes;
    }

    /**
     * Extract possible database prefixes from a plugin folder name
     */
    private function extract_prefixes_from_name($name) {
        $prefixes = [];

        // Clean the name
        $clean = strtolower($name);

        // Add full name (with dashes converted to underscores)
        $prefixes[] = str_replace('-', '_', $clean);

        // Remove 'wp-' or 'wp_' prefix if present
        $without_wp = preg_replace('/^wp[-_]/', '', $clean);
        if ($without_wp !== $clean) {
            $prefixes[] = str_replace('-', '_', $without_wp);
        }

        // Extract abbreviation (first letter of each word)
        $words = preg_split('/[-_]/', $clean);
        if (count($words) > 1) {
            $abbrev = '';
            foreach ($words as $word) {
                if (!empty($word) && $word !== 'wp' && $word !== 'pro' && $word !== 'lite') {
                    $abbrev .= $word[0];
                }
            }
            if (strlen($abbrev) >= 2) {
                $prefixes[] = $abbrev;
            }
        }

        // Add first segment before dash/underscore
        if (count($words) > 0 && strlen($words[0]) >= 3) {
            $prefixes[] = $words[0];
        }

        return array_unique(array_filter($prefixes));
    }

    /**
     * Get all prefixes (known + dynamic + custom user mappings)
     * Uses memoization to avoid repeated expensive lookups during a single scan
     *
     * @param bool $force_refresh Force refresh the cache
     */
    private function get_all_plugin_prefixes($force_refresh = false) {
        // Return cached result if available
        if (!$force_refresh && $this->cached_all_prefixes !== null) {
            return $this->cached_all_prefixes;
        }

        $settings = $this->get_settings();
        $custom_mappings = isset($settings['custom_prefix_mappings']) ? $settings['custom_prefix_mappings'] : [];

        // Start with known prefixes
        $all_prefixes = $this->known_plugin_prefixes;

        // Add dynamic prefixes
        $dynamic = $this->get_dynamic_plugin_prefixes();
        foreach ($dynamic as $prefix => $info) {
            if (!isset($all_prefixes[$prefix])) {
                $all_prefixes[$prefix] = $info;
            }
        }

        // Add custom user mappings (these take highest priority)
        foreach ($custom_mappings as $prefix => $plugin_file) {
            if (!empty($plugin_file)) {
                $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file, false, false);
                $all_prefixes[$prefix] = [
                    'plugin_file' => $plugin_file,
                    'name' => !empty($plugin_data['Name']) ? $plugin_data['Name'] : basename(dirname($plugin_file)),
                    'custom' => true,
                ];
            }
        }

        // Cache the result
        $this->cached_all_prefixes = $all_prefixes;

        return $all_prefixes;
    }

    /**
     * Clear the prefix cache (call when prefix mappings change)
     */
    private function clear_prefix_cache() {
        $this->cached_all_prefixes = null;
    }

    /**
     * Register admin page under Switchboard menu
     */
    public function register_admin_page() {
        add_submenu_page(
            'switchboard',
            __('Cleanup Removed Plugins', 'wpswitchboard'),
            __('Cleanup Removed Plugins', 'wpswitchboard'),
            'manage_options',
            'switchboard-cleanup-removed-plugins',
            [$this, 'render_admin_page']
        );
    }

    /**
     * Enqueue admin assets
     */
    public function enqueue_assets($hook) {
        $is_page = (
            (isset($_GET['page']) && $_GET['page'] === 'switchboard-cleanup-removed-plugins') ||
            strpos($hook, 'switchboard-cleanup-removed-plugins') !== false
        );

        if (!$is_page) {
            return;
        }

        // Enqueue Switchboard admin CSS for CSS variables
        if (defined('SWITCHBOARD_URL') && defined('SWITCHBOARD_VERSION')) {
            wp_enqueue_style(
                'switchboard-admin',
                SWITCHBOARD_URL . 'switchboard-admin.css',
                [],
                SWITCHBOARD_VERSION
            );
        }

        wp_enqueue_style(
            'switchboard-cleanup-removed-plugins',
            SWITCHBOARD_URL . 'assets/cleanup-removed-plugins/admin.css',
            ['switchboard-admin'],
            SWITCHBOARD_VERSION
        );

        wp_enqueue_script(
            'switchboard-cleanup-removed-plugins',
            SWITCHBOARD_URL . 'assets/cleanup-removed-plugins/admin.js',
            ['jquery'],
            SWITCHBOARD_VERSION,
            true
        );

        // Get active plugins for the link dropdown
        $active_plugins_list = [];
        foreach ($this->get_active_plugins() as $plugin_file) {
            $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file, false, false);
            $active_plugins_list[] = [
                'file' => $plugin_file,
                'name' => !empty($plugin_data['Name']) ? $plugin_data['Name'] : basename(dirname($plugin_file)),
            ];
        }
        usort($active_plugins_list, function($a, $b) {
            return strcasecmp($a['name'], $b['name']);
        });

        wp_localize_script('switchboard-cleanup-removed-plugins', 'switchboardCRP', [
            'ajaxurl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('switchboard_crp'),
            'settings' => $this->get_settings(),
            'activePlugins' => $active_plugins_list,
            'i18n' => [
                'scanning' => __('Scanning...', 'wpswitchboard'),
                'scanComplete' => __('Scan complete!', 'wpswitchboard'),
                'rescan' => __('Rescan Database', 'wpswitchboard'),
                'deleting' => __('Deleting...', 'wpswitchboard'),
                'deleted' => __('Deleted successfully', 'wpswitchboard'),
                'error' => __('An error occurred', 'wpswitchboard'),
                'noSelection' => __('Please select items to delete', 'wpswitchboard'),
                'noTables' => __('No orphaned tables found!', 'wpswitchboard'),
                'noOptions' => __('No orphaned options found!', 'wpswitchboard'),
                'noCrons' => __('No zombie cron jobs found!', 'wpswitchboard'),
                'ignored' => __('Item will be ignored in future scans', 'wpswitchboard'),
                'linkToPlugin' => __('Link to plugin...', 'wpswitchboard'),
                'prefixLinked' => __('Prefix linked - rescanning...', 'wpswitchboard'),
                // Confirmation modal strings
                'confirmDeleteTitle' => __('Confirm Delete', 'wpswitchboard'),
                'confirmBulkTitle' => __('Delete Selected Items', 'wpswitchboard'),
                'confirmDeleteTable' => __('Delete this table? This will permanently remove the table and all its data.', 'wpswitchboard'),
                'confirmDeleteOption' => __('Delete this option?', 'wpswitchboard'),
                'confirmDeleteCron' => __('Delete this scheduled task?', 'wpswitchboard'),
                /* translators: %d is the number of selected items */
                'confirmBulkTables' => __('Delete %d selected tables? This will permanently remove all data.', 'wpswitchboard'),
                /* translators: %d is the number of selected items */
                'confirmBulkOptions' => __('Delete %d selected options?', 'wpswitchboard'),
                /* translators: %d is the number of selected items */
                'confirmBulkCrons' => __('Delete %d selected scheduled tasks?', 'wpswitchboard'),
                // Truncation warnings
                'optionsTruncated' => __('Showing first 200 results. Clean up some items and re-scan to see more.', 'wpswitchboard'),
                // Ignored items
                'ignoredItems' => __('Ignored Items', 'wpswitchboard'),
                'noIgnoredItems' => __('No ignored items', 'wpswitchboard'),
                'unignore' => __('Un-ignore', 'wpswitchboard'),
                'unignored' => __('Item will appear in future scans', 'wpswitchboard'),
                /* translators: %d is the number of ignored items */
                'ignoredCount' => __('%d ignored', 'wpswitchboard'),
                // Risk tooltips
                'riskLow' => __('Low risk: Source plugin identified but inactive', 'wpswitchboard'),
                'riskMedium' => __('Medium risk: Source unknown - verify before deleting', 'wpswitchboard'),
                'riskHigh' => __('High risk: May be used by active components', 'wpswitchboard'),
                'riskCronNoCallback' => __('No registered callback - safe to remove', 'wpswitchboard'),
                'riskCronHasCallback' => __('Has active callback - may still be in use', 'wpswitchboard'),
                // Sorting
                'sortAsc' => __('Sort ascending', 'wpswitchboard'),
                'sortDesc' => __('Sort descending', 'wpswitchboard'),
            ],
        ]);
    }

    /**
     * Render the admin page
     */
    public function render_admin_page() {
        include SWITCHBOARD_PATH . 'assets/cleanup-removed-plugins/admin.php';
    }

    /**
     * Get module settings
     */
    public function get_settings() {
        $defaults = [
            'last_scan' => null,
            'cleanup_log' => [],
            'ignored_tables' => [],
            'ignored_options' => [],
            'ignored_crons' => [],
            'custom_prefix_mappings' => [], // User-defined prefix to plugin mappings
        ];

        return wp_parse_args(get_option($this->option_key, []), $defaults);
    }

    /**
     * Register with data registry for cleanup
     */
    private function register_with_data_registry() {
        if (class_exists('Switchboard_Data_Registry')) {
            $registry = Switchboard_Data_Registry::get_instance();
            $registry->auto_register($this, __FILE__);
        }
    }

    /**
     * Register data locations for cleanup system
     */
    public function register_data_locations() {
        return [
            'options' => [
                'switchboard_cleanup-removed-plugins_settings',
            ],
            'option_patterns' => [],
            'tables' => [],
            'post_meta' => [],
            'user_meta' => [],
            'term_meta' => [],
            'transients' => [],
        ];
    }

    // =========================================================================
    // DETECTION METHODS
    // =========================================================================

    /**
     * Get list of active plugins
     */
    private function get_active_plugins() {
        $active = get_option('active_plugins', []);
        // Also include network-active plugins for multisite
        if (is_multisite()) {
            $network_active = get_site_option('active_sitewide_plugins', []);
            $active = array_merge($active, array_keys($network_active));
        }
        return $active;
    }

    /**
     * Check if a plugin is currently active
     */
    private function is_plugin_active($plugin_file) {
        if ($plugin_file === null) {
            return false;
        }
        $active_plugins = $this->get_active_plugins();
        return in_array($plugin_file, $active_plugins, true);
    }

    /**
     * Detect orphaned database tables
     */
    public function detect_orphaned_tables() {
        global $wpdb;

        $orphaned = [];
        $prefix = $wpdb->prefix;

        // Get all tables in the database
        $tables = $wpdb->get_col("SHOW TABLES LIKE '{$prefix}%'");

        // Build list of core table names with prefix
        $core_with_prefix = array_map(function($table) use ($prefix) {
            return $prefix . $table;
        }, $this->core_tables);

        // Additional core tables for multisite
        $multisite_tables = [
            'blogs', 'blog_versions', 'blogmeta', 'registration_log',
            'signups', 'site', 'sitemeta'
        ];
        $multisite_with_prefix = array_map(function($table) use ($prefix) {
            return $prefix . $table;
        }, $multisite_tables);

        $all_core = array_merge($core_with_prefix, $multisite_with_prefix);

        // Get ignored tables from settings
        $settings = $this->get_settings();
        $ignored = isset($settings['ignored_tables']) ? $settings['ignored_tables'] : [];

        foreach ($tables as $table) {
            // Skip core tables
            if (in_array($table, $all_core, true)) {
                continue;
            }

            // Skip ignored tables
            if (in_array($table, $ignored, true)) {
                continue;
            }

            // Try to identify the source plugin
            $source = $this->identify_table_source($table);

            // If we found a source plugin and it's active, skip
            if ($source && isset($source['plugin_file']) && $this->is_plugin_active($source['plugin_file'])) {
                continue;
            }

            // Get table size
            $size_result = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT DATA_LENGTH + INDEX_LENGTH as size, TABLE_ROWS as row_count
                     FROM information_schema.TABLES
                     WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s",
                    DB_NAME,
                    $table
                )
            );

            $orphaned[] = [
                'name' => $table,
                'size' => $size_result ? (int) $size_result->size : 0,
                'rows' => $size_result ? (int) $size_result->row_count : 0,
                'source' => $source,
                'risk' => $source ? 'low' : 'medium', // Unknown source = medium risk
            ];
        }

        return $orphaned;
    }

    /**
     * Identify the source plugin for a table
     */
    private function identify_table_source($table_name) {
        global $wpdb;

        // Remove the WordPress prefix
        $table_without_prefix = preg_replace('/^' . preg_quote($wpdb->prefix, '/') . '/', '', $table_name);

        // Get all prefixes (known + dynamic + custom)
        $all_prefixes = $this->get_all_plugin_prefixes();

        // Check against all prefixes (exact match at start first)
        foreach ($all_prefixes as $prefix => $info) {
            if (stripos($table_without_prefix, $prefix) === 0) {
                return $info;
            }
        }

        // Try to extract a meaningful prefix from the table name
        $parts = explode('_', $table_without_prefix);
        if (count($parts) > 1) {
            $guessed_prefix = $parts[0];
            // Check if this prefix matches any known plugin
            foreach ($all_prefixes as $prefix => $info) {
                if (stripos($prefix, $guessed_prefix) === 0 || stripos($guessed_prefix, $prefix) === 0) {
                    return $info;
                }
            }
            // Return unknown but with guessed prefix - include prefix for UI linking
            return [
                'plugin_file' => null,
                'name' => ucwords(str_replace('_', ' ', $guessed_prefix)) . ' (Unknown)',
                'guessed' => true,
                'guessed_prefix' => $guessed_prefix,
            ];
        }

        return null;
    }

    /**
     * Detect orphaned options
     */
    public function detect_orphaned_options() {
        global $wpdb;

        $orphaned = [];
        $settings = $this->get_settings();
        $ignored = isset($settings['ignored_options']) ? $settings['ignored_options'] : [];

        // Get all prefixes (known + dynamic + custom)
        $all_prefixes = $this->get_all_plugin_prefixes();

        // Get all options that match plugin prefixes
        foreach ($all_prefixes as $prefix => $info) {
            // Skip if plugin is active
            if ($this->is_plugin_active($info['plugin_file'])) {
                continue;
            }

            // Search for options with this prefix
            $options = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT option_id, option_name, LENGTH(option_value) as size, autoload
                     FROM $wpdb->options
                     WHERE option_name LIKE %s
                     AND option_name NOT LIKE %s
                     ORDER BY size DESC
                     LIMIT 100",
                    $wpdb->esc_like($prefix) . '%',
                    $wpdb->esc_like('_transient_') . '%'
                )
            );

            foreach ($options as $option) {
                // Skip protected options
                if ($this->is_protected_option($option->option_name)) {
                    continue;
                }

                // Skip ignored options
                if (in_array($option->option_name, $ignored, true)) {
                    continue;
                }

                $orphaned[] = [
                    'id' => $option->option_id,
                    'name' => $option->option_name,
                    'size' => (int) $option->size,
                    'autoload' => in_array($option->autoload, ['yes', 'on'], true) ? 'yes' : 'no',
                    'source' => $info,
                ];
            }
        }

        // Also look for options that look like plugin options but don't match known prefixes
        // These typically have underscores and longer names
        $potential_orphans = $wpdb->get_results(
            "SELECT option_id, option_name, LENGTH(option_value) as size, autoload
             FROM $wpdb->options
             WHERE option_name REGEXP '^[a-z]+_[a-z_]+$'
             AND option_name NOT LIKE '_transient%'
             AND option_name NOT LIKE '_site_transient%'
             AND LENGTH(option_name) > 10
             ORDER BY size DESC
             LIMIT 200"
        );

        foreach ($potential_orphans as $option) {
            // Skip if already in orphaned list
            $already_found = false;
            foreach ($orphaned as $o) {
                if ($o['name'] === $option->option_name) {
                    $already_found = true;
                    break;
                }
            }
            if ($already_found) {
                continue;
            }

            // Skip protected options
            if ($this->is_protected_option($option->option_name)) {
                continue;
            }

            // Skip ignored
            if (in_array($option->option_name, $ignored, true)) {
                continue;
            }

            // Try to identify source
            $source = $this->identify_option_source($option->option_name);

            // If we identified an active plugin, skip
            if ($source && isset($source['plugin_file']) && $this->is_plugin_active($source['plugin_file'])) {
                continue;
            }

            $orphaned[] = [
                'id' => $option->option_id,
                'name' => $option->option_name,
                'size' => (int) $option->size,
                'autoload' => in_array($option->autoload, ['yes', 'on'], true) ? 'yes' : 'no',
                'source' => $source,
            ];
        }

        // Remove duplicates and sort by size
        $unique = [];
        $seen = [];
        foreach ($orphaned as $option) {
            if (!isset($seen[$option['name']])) {
                $unique[] = $option;
                $seen[$option['name']] = true;
            }
        }

        usort($unique, function($a, $b) {
            return $b['size'] - $a['size'];
        });

        return array_slice($unique, 0, 200); // Limit to 200 for performance
    }

    /**
     * Check if an option is protected
     */
    private function is_protected_option($option_name) {
        foreach ($this->protected_option_prefixes as $prefix) {
            if (strpos($option_name, $prefix) === 0 || $option_name === $prefix) {
                return true;
            }
        }
        return false;
    }

    /**
     * Identify the source plugin for an option
     */
    private function identify_option_source($option_name) {
        // Get all prefixes (known + dynamic + custom)
        $all_prefixes = $this->get_all_plugin_prefixes();

        foreach ($all_prefixes as $prefix => $info) {
            if (stripos($option_name, $prefix) === 0) {
                return $info;
            }
        }

        // Try to guess from the option name
        $parts = explode('_', $option_name);
        if (count($parts) > 1) {
            $guessed_prefix = strtolower($parts[0]);
            return [
                'plugin_file' => null,
                'name' => ucwords($parts[0]) . ' (Unknown)',
                'guessed' => true,
                'guessed_prefix' => $guessed_prefix,
            ];
        }

        return null;
    }

    /**
     * Detect orphaned cron jobs
     */
    public function detect_orphaned_crons() {
        $orphaned = [];
        $settings = $this->get_settings();
        $ignored = isset($settings['ignored_crons']) ? $settings['ignored_crons'] : [];

        $crons = _get_cron_array();
        if (empty($crons)) {
            return [];
        }

        // WordPress core cron hooks that should be protected
        $core_crons = [
            'wp_scheduled_delete', 'wp_scheduled_auto_draft_delete', 'wp_version_check',
            'wp_update_plugins', 'wp_update_themes', 'wp_site_health_scheduled_check',
            'delete_expired_transients', 'recovery_mode_clean_expired_keys',
            'wp_privacy_delete_old_export_files', 'wp_https_detection',
            'wp_update_user_counts', 'wp_delete_temp_updater_backups',
        ];

        // Switchboard's own crons
        $switchboard_crons = [
            'switchboard_database_cleaner_cron', 'switchboard_clean_transients',
        ];

        $protected_crons = array_merge($core_crons, $switchboard_crons);

        foreach ($crons as $timestamp => $cron_events) {
            foreach ($cron_events as $hook => $events) {
                // Skip core crons
                if (in_array($hook, $protected_crons, true)) {
                    continue;
                }

                // Skip ignored crons
                if (in_array($hook, $ignored, true)) {
                    continue;
                }

                // Try to identify source
                $source = $this->identify_cron_source($hook);

                // If we identified an active plugin, skip
                if ($source && isset($source['plugin_file']) && $this->is_plugin_active($source['plugin_file'])) {
                    continue;
                }

                // Check if the hook has any registered callbacks
                $has_callback = has_action($hook);

                // If it has a callback, the plugin might still be active in some form
                // But if it doesn't have a callback, it's definitely orphaned
                foreach ($events as $key => $event) {
                    $orphaned[] = [
                        'hook' => $hook,
                        'timestamp' => $timestamp,
                        'schedule' => isset($event['schedule']) ? $event['schedule'] : 'single',
                        'args' => isset($event['args']) ? $event['args'] : [],
                        'next_run' => date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $timestamp),
                        'source' => $source,
                        'has_callback' => $has_callback,
                        'risk' => $has_callback ? 'medium' : 'low',
                    ];
                }
            }
        }

        // Remove duplicates by hook name
        $unique = [];
        $seen = [];
        foreach ($orphaned as $cron) {
            if (!isset($seen[$cron['hook']])) {
                $unique[] = $cron;
                $seen[$cron['hook']] = true;
            }
        }

        return $unique;
    }

    /**
     * Identify the source plugin for a cron job
     */
    private function identify_cron_source($hook_name) {
        // Get all prefixes (known + dynamic + custom)
        $all_prefixes = $this->get_all_plugin_prefixes();

        foreach ($all_prefixes as $prefix => $info) {
            if (stripos($hook_name, $prefix) === 0) {
                return $info;
            }
        }

        // Try to guess from the hook name
        $parts = explode('_', $hook_name);
        if (count($parts) > 1) {
            $guessed_prefix = strtolower($parts[0]);
            return [
                'plugin_file' => null,
                'name' => ucwords($parts[0]) . ' (Unknown)',
                'guessed' => true,
                'guessed_prefix' => $guessed_prefix,
            ];
        }

        return null;
    }

    /**
     * Get summary statistics
     */
    public function get_summary() {
        $tables = $this->detect_orphaned_tables();
        $options = $this->detect_orphaned_options();
        $crons = $this->detect_orphaned_crons();

        $table_size = array_sum(array_column($tables, 'size'));
        $option_size = array_sum(array_column($options, 'size'));

        return [
            'tables' => [
                'count' => count($tables),
                'size' => $table_size,
            ],
            'options' => [
                'count' => count($options),
                'size' => $option_size,
            ],
            'crons' => [
                'count' => count($crons),
            ],
            'total_size' => $table_size + $option_size,
        ];
    }

    // =========================================================================
    // CLEANUP METHODS
    // =========================================================================

    /**
     * Delete a database table
     */
    public function delete_table($table_name) {
        global $wpdb;

        // Validate table name (alphanumeric and underscores only)
        if (!preg_match('/^[a-zA-Z0-9_]+$/', $table_name)) {
            return false;
        }

        // Verify table belongs to this WordPress installation
        $prefix = $wpdb->prefix;
        if (strpos($table_name, $prefix) !== 0) {
            return false;
        }

        // Verify table is not a core table
        $core_with_prefix = array_map(function($table) use ($prefix) {
            return $prefix . $table;
        }, $this->core_tables);

        if (in_array($table_name, $core_with_prefix, true)) {
            return false;
        }

        // Drop the table
        $result = $wpdb->query("DROP TABLE IF EXISTS `{$table_name}`");

        if ($result !== false) {
            $this->log_cleanup('table', $table_name);
        }

        return $result !== false;
    }

    /**
     * Delete options by pattern or specific name
     */
    public function delete_options($option_names) {
        global $wpdb;

        if (!is_array($option_names)) {
            $option_names = [$option_names];
        }

        $deleted = 0;

        foreach ($option_names as $option_name) {
            // Skip protected options
            if ($this->is_protected_option($option_name)) {
                continue;
            }

            if (delete_option($option_name)) {
                $deleted++;
                $this->log_cleanup('option', $option_name);
            }
        }

        return $deleted;
    }

    /**
     * Delete a cron job
     */
    public function delete_cron($hook, $timestamp = null, $args = []) {
        // WordPress core crons should be protected (must match detect_orphaned_crons list)
        $core_crons = [
            'wp_scheduled_delete', 'wp_scheduled_auto_draft_delete', 'wp_version_check',
            'wp_update_plugins', 'wp_update_themes', 'wp_site_health_scheduled_check',
            'delete_expired_transients', 'recovery_mode_clean_expired_keys',
            'wp_privacy_delete_old_export_files', 'wp_https_detection',
            'wp_update_user_counts', 'wp_delete_temp_updater_backups',
        ];

        if (in_array($hook, $core_crons, true)) {
            return false;
        }

        if ($timestamp !== null) {
            // Unschedule specific event
            $result = wp_unschedule_event($timestamp, $hook, $args);
        } else {
            // Clear all events for this hook
            wp_clear_scheduled_hook($hook, $args);
            $result = true;
        }

        if ($result) {
            $this->log_cleanup('cron', $hook);
        }

        return $result;
    }

    /**
     * Log a cleanup action
     */
    private function log_cleanup($type, $item) {
        $settings = $this->get_settings();
        $log = isset($settings['cleanup_log']) ? $settings['cleanup_log'] : [];

        $log[] = [
            'type' => $type,
            'item' => $item,
            'time' => current_time('mysql'),
            'timestamp' => time(),
        ];

        // Keep last 100 entries
        $log = array_slice($log, -100);

        $settings['cleanup_log'] = $log;
        update_option($this->option_key, $settings);
    }

    // =========================================================================
    // AJAX HANDLERS
    // =========================================================================

    /**
     * AJAX: Perform full scan
     */
    public function ajax_scan() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        // Clear prefix cache to ensure fresh results
        $this->clear_prefix_cache();

        $tables = $this->detect_orphaned_tables();
        $options = $this->detect_orphaned_options();
        $crons = $this->detect_orphaned_crons();

        // Update last scan time
        $settings = $this->get_settings();
        $settings['last_scan'] = current_time('mysql');
        update_option($this->option_key, $settings);

        // Check for truncation (limits are 200 for options)
        $options_truncated = count($options) >= 200;

        wp_send_json_success([
            'tables' => $tables,
            'options' => $options,
            'crons' => $crons,
            'summary' => [
                'tables_count' => count($tables),
                'tables_size' => array_sum(array_column($tables, 'size')),
                'options_count' => count($options),
                'options_size' => array_sum(array_column($options, 'size')),
                'crons_count' => count($crons),
                'last_scan' => __('Just now', 'wpswitchboard'),
            ],
            'truncated' => [
                'options' => $options_truncated,
            ],
            'ignored' => [
                'tables' => count($settings['ignored_tables']),
                'options' => count($settings['ignored_options']),
                'crons' => count($settings['ignored_crons']),
            ],
        ]);
    }

    /**
     * AJAX: Delete a table
     */
    public function ajax_delete_table() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $table = isset($_POST['table']) ? sanitize_text_field($_POST['table']) : '';

        if (empty($table)) {
            wp_send_json_error(['message' => __('Invalid table name', 'wpswitchboard')]);
        }

        $result = $this->delete_table($table);

        if ($result) {
            wp_send_json_success(['message' => __('Table deleted', 'wpswitchboard')]);
        } else {
            wp_send_json_error(['message' => __('Failed to delete table', 'wpswitchboard')]);
        }
    }

    /**
     * AJAX: Delete options
     */
    public function ajax_delete_options() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $options = isset($_POST['options']) ? (array) $_POST['options'] : [];
        $options = array_map('sanitize_text_field', $options);

        if (empty($options)) {
            wp_send_json_error(['message' => __('No options specified', 'wpswitchboard')]);
        }

        $deleted = $this->delete_options($options);

        wp_send_json_success([
            'message' => sprintf(__('Deleted %d option(s)', 'wpswitchboard'), $deleted),
            'deleted' => $deleted,
        ]);
    }

    /**
     * AJAX: Delete a cron job
     */
    public function ajax_delete_cron() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $hook = isset($_POST['hook']) ? sanitize_text_field($_POST['hook']) : '';
        $timestamp = isset($_POST['timestamp']) ? (int) $_POST['timestamp'] : null;

        if (empty($hook)) {
            wp_send_json_error(['message' => __('Invalid cron hook', 'wpswitchboard')]);
        }

        $result = $this->delete_cron($hook, $timestamp);

        if ($result) {
            wp_send_json_success(['message' => __('Cron job deleted', 'wpswitchboard')]);
        } else {
            wp_send_json_error(['message' => __('Failed to delete cron job', 'wpswitchboard')]);
        }
    }

    /**
     * AJAX: Bulk cleanup
     */
    public function ajax_bulk_cleanup() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $tables = isset($_POST['tables']) ? (array) $_POST['tables'] : [];
        $options = isset($_POST['options']) ? (array) $_POST['options'] : [];
        $crons = isset($_POST['crons']) ? (array) $_POST['crons'] : [];

        $results = [
            'tables' => 0,
            'options' => 0,
            'crons' => 0,
        ];

        // Delete tables
        foreach ($tables as $table) {
            $table = sanitize_text_field($table);
            if ($this->delete_table($table)) {
                $results['tables']++;
            }
        }

        // Delete options
        $options = array_map('sanitize_text_field', $options);
        $results['options'] = $this->delete_options($options);

        // Delete crons
        foreach ($crons as $hook) {
            $hook = sanitize_text_field($hook);
            if ($this->delete_cron($hook)) {
                $results['crons']++;
            }
        }

        wp_send_json_success([
            'message' => sprintf(
                __('Cleaned up: %d tables, %d options, %d cron jobs', 'wpswitchboard'),
                $results['tables'],
                $results['options'],
                $results['crons']
            ),
            'results' => $results,
        ]);
    }

    /**
     * AJAX: Save settings (ignore list)
     */
    public function ajax_save_settings() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $settings = $this->get_settings();

        // Update ignore lists
        if (isset($_POST['ignore_table'])) {
            $table = sanitize_text_field($_POST['ignore_table']);
            if (!in_array($table, $settings['ignored_tables'], true)) {
                $settings['ignored_tables'][] = $table;
            }
        }

        if (isset($_POST['ignore_option'])) {
            $option = sanitize_text_field($_POST['ignore_option']);
            if (!in_array($option, $settings['ignored_options'], true)) {
                $settings['ignored_options'][] = $option;
            }
        }

        if (isset($_POST['ignore_cron'])) {
            $cron = sanitize_text_field($_POST['ignore_cron']);
            if (!in_array($cron, $settings['ignored_crons'], true)) {
                $settings['ignored_crons'][] = $cron;
            }
        }

        update_option($this->option_key, $settings);

        wp_send_json_success(['message' => __('Settings saved', 'wpswitchboard')]);
    }

    /**
     * AJAX: Get option value for preview
     */
    public function ajax_get_option_value() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $option_name = isset($_POST['option_name']) ? sanitize_text_field($_POST['option_name']) : '';

        if (empty($option_name)) {
            wp_send_json_error(['message' => __('Invalid option name', 'wpswitchboard')]);
        }

        $value = get_option($option_name);

        // Format the value for display
        if (is_array($value) || is_object($value)) {
            $formatted = print_r($value, true);
        } else {
            $formatted = (string) $value;
        }

        // Truncate if too long
        if (strlen($formatted) > 5000) {
            $formatted = substr($formatted, 0, 5000) . "\n\n... (truncated)";
        }

        wp_send_json_success([
            'value' => $formatted,
            'type' => gettype($value),
        ]);
    }

    /**
     * AJAX: Link a prefix to an active plugin
     */
    public function ajax_link_prefix() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $prefix = isset($_POST['prefix']) ? sanitize_key($_POST['prefix']) : '';
        $plugin_file = isset($_POST['plugin_file']) ? sanitize_text_field($_POST['plugin_file']) : '';

        if (empty($prefix)) {
            wp_send_json_error(['message' => __('Invalid prefix', 'wpswitchboard')]);
        }

        $settings = $this->get_settings();

        if (!isset($settings['custom_prefix_mappings'])) {
            $settings['custom_prefix_mappings'] = [];
        }

        if (empty($plugin_file)) {
            // Remove the mapping
            unset($settings['custom_prefix_mappings'][$prefix]);
        } else {
            // Add/update the mapping
            $settings['custom_prefix_mappings'][$prefix] = $plugin_file;
        }

        update_option($this->option_key, $settings);

        // Clear prefix cache so next scan uses updated mappings
        $this->clear_prefix_cache();

        wp_send_json_success([
            'message' => __('Prefix mapping updated', 'wpswitchboard'),
            'prefix' => $prefix,
            'plugin_file' => $plugin_file,
        ]);
    }

    /**
     * AJAX: Get list of active plugins for linking UI
     */
    public function ajax_get_active_plugins() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $active_plugins = $this->get_active_plugins();
        $plugins_list = [];

        foreach ($active_plugins as $plugin_file) {
            $plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file, false, false);
            $plugins_list[] = [
                'file' => $plugin_file,
                'name' => !empty($plugin_data['Name']) ? $plugin_data['Name'] : basename(dirname($plugin_file)),
            ];
        }

        // Sort alphabetically by name
        usort($plugins_list, function($a, $b) {
            return strcasecmp($a['name'], $b['name']);
        });

        wp_send_json_success([
            'plugins' => $plugins_list,
        ]);
    }

    /**
     * AJAX: Get list of ignored items
     */
    public function ajax_get_ignored() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $settings = $this->get_settings();

        wp_send_json_success([
            'tables' => isset($settings['ignored_tables']) ? $settings['ignored_tables'] : [],
            'options' => isset($settings['ignored_options']) ? $settings['ignored_options'] : [],
            'crons' => isset($settings['ignored_crons']) ? $settings['ignored_crons'] : [],
        ]);
    }

    /**
     * AJAX: Un-ignore an item
     */
    public function ajax_unignore() {
        check_ajax_referer('switchboard_crp', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => __('Unauthorized', 'wpswitchboard')]);
        }

        $type = isset($_POST['type']) ? sanitize_key($_POST['type']) : '';
        $item = isset($_POST['item']) ? sanitize_text_field($_POST['item']) : '';

        if (!in_array($type, ['table', 'option', 'cron'], true) || empty($item)) {
            wp_send_json_error(['message' => __('Invalid request', 'wpswitchboard')]);
        }

        $settings = $this->get_settings();
        $key = 'ignored_' . $type . 's';

        if (isset($settings[$key]) && is_array($settings[$key])) {
            $settings[$key] = array_values(array_diff($settings[$key], [$item]));
            update_option($this->option_key, $settings);
        }

        wp_send_json_success([
            'message' => __('Item will be included in future scans', 'wpswitchboard'),
        ]);
    }

}

// Initialize
$switchboard_instance = new SwitchboardCleanupRemovedPlugins();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
