<?php
/**
 * Name: Transients Cleaner
 * Description: Clean expired and orphaned transients from WordPress database to optimize performance, reduce database size, and remove temporary data left by plugins and themes
 * Category: Optimization
 * Icon: -
 * HasSettings: true
 * UseCases: [
 *   {
 *     "title": "Database Optimization",
 *     "description": "Remove expired transients that WordPress doesn't auto-delete, reducing database bloat and improving query performance."
 *   },
 *   {
 *     "title": "Scheduled Cleanup",
 *     "description": "Automatically clean transients daily, weekly, or monthly to keep your database lean and fast."
 *   },
 *   {
 *     "title": "Manual Control",
 *     "description": "Review transient counts and manually trigger cleanup when needed with detailed statistics."
 *   }
 * ]
 * Added: 2025-09-08
 * Tags: transients, database, db, cleanup, optimization, performance, cache
 * WhereToFind: {
 *   "location": "Switchboard Settings",
 *   "url": "",
 *   "instructions": "Configure automatic cleanup schedule or manually trigger cleanup in module settings. Removes expired transients from database to optimize performance.",
 *   "newTab": false
 * }
 */

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

class SwitchboardTransientsCleaner {

    private $option_key = 'switchboard_transients_cleaner';

    public function __construct() {
        // Hook into scheduled cleanup
        add_action('switchboard_clean_transients', [$this, 'clean_transients']);

        // AJAX handler for manual cleanup
        add_action('wp_ajax_switchboard_clean_transients_now', [$this, 'ajax_clean_transients']);
    }

    /**
     * Called when module is activated
     * Schedules cron events based on module settings
     */
    public function activate() {
        $settings = get_option($this->option_key, []);
        $auto_clean = isset($settings['auto_clean']) ? $settings['auto_clean'] : 'yes';
        $schedule = isset($settings['schedule']) ? $settings['schedule'] : 'daily';

        if ($auto_clean === 'yes' && !wp_next_scheduled('switchboard_clean_transients')) {
            wp_schedule_event(time(), $schedule, 'switchboard_clean_transients');
        }
    }

    /**
     * Called when module is deactivated
     * Clears all scheduled cron events
     */
    public function deactivate() {
        wp_clear_scheduled_hook('switchboard_clean_transients');
    }

    public function ajax_clean_transients() {
        check_ajax_referer('switchboard_clean_transients', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Permission denied', 'wpswitchboard')]);
        }

        $cleaned = $this->clean_transients();

        wp_send_json_success([
            'cleaned' => $cleaned,
            // translators: %d is the number of expired transients cleaned
            'message' => sprintf(esc_html__('%d expired transients cleaned!', 'wpswitchboard'), $cleaned)
        ]);
    }

    private function setup_schedule() {
        $settings = get_option($this->option_key, []);
        $auto_clean = isset($settings['auto_clean']) ? $settings['auto_clean'] : 'yes';
        $schedule = isset($settings['schedule']) ? $settings['schedule'] : 'daily';

        // Clear existing schedule
        wp_clear_scheduled_hook('switchboard_clean_transients');

        // Schedule new event if auto-clean is enabled
        if ($auto_clean === 'yes') {
            wp_schedule_event(time(), $schedule, 'switchboard_clean_transients');
        }
    }

    /**
     * Render settings panel for Transients Cleaner
     * 
     * Displays interactive transients management interface with:
     * - Informational box explaining transients
     * - Real-time statistics showing expired transients count
     * - Manual cleanup button with AJAX functionality
     * - Auto-clean toggle (enabled/disabled)
     * - Cleanup schedule dropdown (daily, twice daily, weekly)
     * - Visual feedback for cleanup results
     * - jQuery-based AJAX with success/info states
     * 
     * Transients are WordPress's temporary cache storage system. Over time, expired
     * transients accumulate in the database if WordPress's auto-cleanup fails. This
     * module provides both manual and automatic cleanup tools with detailed statistics.
     * 
     * The UI queries the database on render to show current transient counts and
     * provides a button that triggers AJAX cleanup via ajax_clean_transients method.
     * Settings control automatic scheduled cleanup using WordPress's cron system.
     * 
     * @param array $settings Current module settings with 'auto_clean' and 'schedule' keys
     * @return void Outputs HTML directly with inline CSS and jQuery
     */
    public function render_settings($settings = []) {
        global $wpdb;
        $auto_clean = isset($settings['auto_clean']) ? $settings['auto_clean'] : 'yes';
        $schedule = isset($settings['schedule']) ? $settings['schedule'] : 'daily';

        // Get last run info
        $last_run = get_option('switchboard_transients_cleaner_last_run', []);
        $last_run_time = isset($last_run['time']) ? $last_run['time'] : 'Never';
        $last_cleaned = isset($last_run['cleaned']) ? $last_run['cleaned'] : 0;

        // Get cron status
        $next_scheduled = wp_next_scheduled('switchboard_clean_transients');
        $cron_status = $next_scheduled ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_scheduled) : false;

        // Get current stats
        $time = time();
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Counting transients for admin stats display
        $total = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->options WHERE option_name LIKE %s AND option_name NOT LIKE %s", $wpdb->esc_like('_transient_') . '%', $wpdb->esc_like('_transient_timeout_') . '%'));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Counting expired transients for admin stats display
        $expired = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM $wpdb->options WHERE option_name LIKE %s AND option_value < %d", $wpdb->esc_like('_transient_timeout_') . '%', $time));

        // Only count actual expired transients - don't try to calculate orphaned
        // (Some transients don't have timeouts and that's normal WordPress behavior)
        $cleanable = $expired;
        ?>
        <input type="hidden" name="module" value="transients-cleaner">

        <style>
        .transients-info-box {
            background: var(--primary-50);
            border: 1px solid var(--primary-200);
            border-left: 4px solid var(--primary-500);
            border-radius: 6px;
            padding: 12px;
            margin: 16px 0;
            font-size: 12px;
            line-height: 1.6;
            color: var(--primary-900);
        }
        .transients-clean-btn {
            background: var(--primary-500);
            color: white;
            border: none;
            border-radius: 6px;
            padding: 10px 20px;
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.2s;
            margin: 12px 0;
        }
        .transients-clean-btn:hover {
            background: var(--primary-600);
            box-shadow: 0 4px 12px rgba(var(--primary-500-rgb), 0.3);
        }
        .transients-feedback {
            display: none;
            margin: 12px 0;
            padding: 12px;
            border-radius: 6px;
            font-size: 13px;
            font-weight: 500;
        }
        .transients-feedback.success {
            background: var(--primary-100);
            border: 1px solid var(--primary-300);
            border-left: 4px solid var(--primary-500);
            color: var(--primary-800);
        }
        .transients-feedback.info {
            background: #fef3c7;
            border: 1px solid #fde047;
            border-left: 4px solid #f59e0b;
            color: #78350f;
        }
        </style>

        <div class="transients-info-box">
            <strong><?php esc_html_e('What are transients?', 'wpswitchboard'); ?></strong> <?php esc_html_e('Transients are WordPress\'s way of storing temporary cached data. Over time, expired transients can accumulate in your database. This module automatically cleans them to keep your database optimized.', 'wpswitchboard'); ?>
        </div>

        <?php if ($cleanable > 0): ?>
        <div class="sb-sp-alert sb-sp-alert-warning">
            <strong><?php
            // translators: %1$d is the number of expired transients, %2$s is the plural suffix
            printf(esc_html__('Found %1$d expired transient%2$s', 'wpswitchboard'), esc_html($cleanable), $cleanable > 1 ? 's' : ''); ?></strong> <?php esc_html_e('that can be cleaned', 'wpswitchboard'); ?>
        </div>
        <?php else: ?>
        <div class="sb-sp-alert-success">
            <strong><?php esc_html_e('All clean!', 'wpswitchboard'); ?></strong> <?php esc_html_e('No expired transients found.', 'wpswitchboard'); ?>
        </div>
        <?php endif; ?>

        <button type="button" class="transients-clean-btn" id="clean-transients-now">
            <?php esc_html_e('Clean Transients Now', 'wpswitchboard'); ?>
        </button>

        <div id="transients-feedback" class="transients-feedback"></div>

        <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
            <div class="sb-sp-field-horizontal-left">
                <label for="setting_auto_clean" class="sb-sp-label"><?php esc_html_e('Automatic Cleanup', 'wpswitchboard'); ?></label>
                <p class="sb-sp-description"><?php esc_html_e('Automatically clean transients on a schedule', 'wpswitchboard'); ?></p>
            </div>
            <select name="setting_auto_clean" id="setting_auto_clean" class="sb-sp-select">
                <option value="yes" <?php selected($auto_clean, 'yes'); ?>><?php esc_html_e('Enabled', 'wpswitchboard'); ?></option>
                <option value="no" <?php selected($auto_clean, 'no'); ?>><?php esc_html_e('Disabled', 'wpswitchboard'); ?></option>
            </select>
        </div>

        <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
            <div class="sb-sp-field-horizontal-left">
                <label for="setting_schedule" class="sb-sp-label"><?php esc_html_e('Cleanup Schedule', 'wpswitchboard'); ?></label>
                <p class="sb-sp-description"><?php esc_html_e('How often to run automatic cleanup', 'wpswitchboard'); ?></p>
            </div>
            <select name="setting_schedule" id="setting_schedule" class="sb-sp-select">
                <option value="daily" <?php selected($schedule, 'daily'); ?>><?php esc_html_e('Daily', 'wpswitchboard'); ?></option>
                <option value="twicedaily" <?php selected($schedule, 'twicedaily'); ?>><?php esc_html_e('Twice Daily', 'wpswitchboard'); ?></option>
                <option value="weekly" <?php selected($schedule, 'weekly'); ?>><?php esc_html_e('Weekly', 'wpswitchboard'); ?></option>
            </select>
        </div>

        <?php if ($auto_clean === 'yes'): ?>
        <div class="sb-sp-group" style="margin-bottom: 24px;">
            <p class="sb-sp-description" style="margin: 0;">
                <?php if ($cron_status): ?>
                    <?php // translators: %s is the date and time of the next scheduled cleanup ?>
                    <span style="color: #16a34a;">[OK]</span> <?php printf(esc_html__('Next scheduled cleanup: %s', 'wpswitchboard'), '<strong>' . esc_html($cron_status) . '</strong>'); ?>
                <?php else: ?>
                    <span style="color: #dc2626;">[X]</span> <?php esc_html_e('Cron not scheduled. Try disabling and re-enabling this module.', 'wpswitchboard'); ?>
                <?php endif; ?>
                <?php if ($last_run_time !== 'Never'): ?>
                    <?php // translators: %1$s is the last run date/time, %2$d is the number of transients cleaned ?>
                    <br><span style="color: #6b7280;"><?php printf(esc_html__('Last run: %1$s (cleaned %2$d)', 'wpswitchboard'), esc_html($last_run_time), esc_html($last_cleaned)); ?></span>
                <?php endif; ?>
            </p>
        </div>
        <?php endif; ?>

        <script>
        jQuery(document).ready(function($) {
            var i18n = <?php echo json_encode([
                'cleaning' => esc_html__('Cleaning...', 'wpswitchboard'),
                'cleanNow' => esc_html__('Clean Transients Now', 'wpswitchboard'),
                'success' => esc_html__('Success!', 'wpswitchboard'),
                // translators: %1$d is the number of transients cleaned, %2$s is the plural suffix
                'cleaned' => esc_html__('Cleaned %1$d transient%2$s from your database.', 'wpswitchboard'),
                'noAction' => esc_html__('No action needed.', 'wpswitchboard'),
                'alreadyClean' => esc_html__('All transients are already clean.', 'wpswitchboard'),
                'error' => esc_html__('Error:', 'wpswitchboard'),
                'failedClean' => esc_html__('Failed to clean transients.', 'wpswitchboard'),
                'failedConnect' => esc_html__('Failed to connect. Please try again.', 'wpswitchboard')
            ]); ?>;

            $('#clean-transients-now').on('click', function() {
                var $btn = $(this);
                var $feedback = $('#transients-feedback');

                $btn.prop('disabled', true).text(i18n.cleaning);
                $feedback.hide().removeClass('success info');

                $.ajax({
                    url: ajaxurl,
                    type: 'POST',
                    data: {
                        action: 'switchboard_clean_transients_now',
                        nonce: '<?php echo esc_attr(wp_create_nonce('switchboard_clean_transients')); ?>'
                    },
                    success: function(response) {
                        if (response.success) {
                            var cleaned = response.data.cleaned;

                            if (cleaned > 0) {
                                var plural = cleaned > 1 ? 's' : '';
                                var message = i18n.cleaned.replace('%1$d', cleaned).replace('%2$s', plural);
                                $feedback.addClass('success')
                                    .html('<strong>' + i18n.success + '</strong> ' + message)
                                    .fadeIn(300);
                            } else {
                                $feedback.addClass('info')
                                    .html('<strong>' + i18n.noAction + '</strong> ' + i18n.alreadyClean)
                                    .fadeIn(300);
                            }

                            // Reset button
                            setTimeout(function() {
                                $btn.prop('disabled', false).text(i18n.cleanNow);
                            }, 1500);
                        } else {
                            $feedback.addClass('info')
                                .html('<strong>' + i18n.error + '</strong> ' + (response.data.message || i18n.failedClean))
                                .fadeIn(300);
                            $btn.prop('disabled', false).text(i18n.cleanNow);
                        }
                    },
                    error: function() {
                        $feedback.addClass('info')
                            .html('<strong>' + i18n.error + '</strong> ' + i18n.failedConnect)
                            .fadeIn(300);
                        $btn.prop('disabled', false).text(i18n.cleanNow);
                    }
                });
            });
        });
        </script>
        <?php
    }

    public function clean_transients() {
        global $wpdb;

        $cleaned = 0;
        $time = time();

        // Only delete expired transients (using delete_transient for proper cleanup)
        // Note: We don't delete "orphaned" transients because WordPress allows
        // transients without timeouts (permanent transients)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Finding expired transients for cleanup
        $expired = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s AND option_value < %d",
                $wpdb->esc_like('_transient_timeout_') . '%',
                $time
            )
        );

        if (is_array($expired) && !empty($expired)) {
            foreach ($expired as $transient) {
                if (is_string($transient)) {
                    $key = str_replace('_transient_timeout_', '', $transient);
                    if (delete_transient($key)) {
                        $cleaned++;
                    }
                }
            }
        }

        // Update last run time
        update_option($this->option_key . '_last_run', [
            'time' => current_time('mysql'),
            'cleaned' => $cleaned
        ]);

        return $cleaned;
    }

    /**
     * Register data locations for cleanup system
     *
     * @return array Data storage locations
     */
    public function register_data_locations() {
        return [
            'options' => [
                'switchboard_transients_cleaner',
                'switchboard_transients_cleaner_last_run',
            ],
            'option_patterns' => [],
            'tables' => [],
            'post_meta' => [],
            'user_meta' => [],
        ];
    }
}

$switchboard_instance = new SwitchboardTransientsCleaner();
Switchboard_Data_Registry::get_instance()->auto_register($switchboard_instance, __FILE__);
