<?php
/**
 * Name: Revisions Control
 * Description: Limit post revisions to reduce database bloat and improve backup speed. Set custom limits or disable revisions entirely, plus clean up existing revisions.
 * Category: Optimization
 * Icon: -
 * UseCases: [
 *   {
 *     "title": "Database Size Control",
 *     "description": "Limit revisions to 5 per post on content-heavy sites with thousands of articles to prevent database from growing to gigabytes."
 *   },
 *   {
 *     "title": "Backup Speed",
 *     "description": "Reduce revision count to speed up database backups on sites where backup plugins struggle with oversized wp_posts tables."
 *   },
 *   {
 *     "title": "Performance Optimization",
 *     "description": "Cap revisions on WooCommerce sites where product descriptions get edited frequently, preventing database bloat."
 *   }
 * ]
 * Added: 2025-07-25
 * Tags: revisions, database, db, optimization, performance, cleanup
 * WhereToFind: {
 *   "location": "Configure in module settings",
 *   "url": "",
 *   "instructions": "Set revision limit and clean up old revisions",
 *   "newTab": false
 * }
 */

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

class SwitchboardRevisionsControl {

    private $settings;

    public function __construct() {
        $this->settings = get_option('switchboard_revisions-control_settings', []);

        add_action('init', [$this, 'set_revision_limit'], 11);
        add_action('wp_ajax_switchboard_clean_revisions', [$this, 'ajax_clean_revisions']);
        add_action('wp_ajax_switchboard_enforce_revision_limit', [$this, 'ajax_enforce_limit']);

        // Auto-enforce limit after each post save
        add_action('post_updated', [$this, 'auto_enforce_limit_on_save'], 20, 1);

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

    private function register_with_data_registry() {
        if (class_exists('Switchboard_Data_Registry')) {
            $registry = Switchboard_Data_Registry::get_instance();
            $registry->auto_register($this, __FILE__);
        }
    }

    public function register_data_locations() {
        return [
            'options' => [
                'switchboard_revisions-control_settings'
            ],
            'option_patterns' => [],
            'tables' => [],
            'post_meta' => [],
            'user_meta' => [],
            'term_meta' => [],
            'transients' => []
        ];
    }

    /**
     * Render settings panel
     *
     * @param array $settings Current module settings
     * @return void Outputs HTML directly
     */
    public function render_settings($settings = []) {
        $limit = isset($settings['limit']) ? intval($settings['limit']) : 5;

        // Get current revision count and posts with excess revisions
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Counting revisions for admin display
        $revision_count = $wpdb->get_var("SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'revision'");

        // Count posts that have more revisions than the limit
        $posts_over_limit = 0;
        $excess_revisions = 0;
        if ($limit > 0) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregation query for admin stats display
            $posts_with_revisions = $wpdb->get_results($wpdb->prepare("
                SELECT post_parent, COUNT(*) as rev_count
                FROM $wpdb->posts
                WHERE post_type = 'revision' AND post_parent > 0
                GROUP BY post_parent
                HAVING rev_count > %d
            ", $limit));
            $posts_over_limit = count($posts_with_revisions);
            foreach ($posts_with_revisions as $post) {
                $excess_revisions += ($post->rev_count - $limit);
            }
        }
        ?>
        <input type="hidden" name="module" value="revisions-control">

        <div class="sb-sp-group sb-sp-field-horizontal" style="margin-bottom: 24px;">
            <div class="sb-sp-field-horizontal-left">
                <label for="setting_limit" class="sb-sp-label"><?php esc_html_e('Maximum Revisions Per Post', 'wpswitchboard'); ?></label>
                <p class="sb-sp-description"><?php esc_html_e('Number of revisions to keep per post. Set to 0 to disable revisions completely. Excess revisions are automatically deleted when posts are saved.', 'wpswitchboard'); ?></p>
            </div>
            <input type="number" name="setting_limit" id="setting_limit" value="<?php echo esc_attr($limit); ?>" min="0" max="100" class="sb-sp-input" style="width: 150px;">
        </div>

        <div class="sb-sp-divider"></div>

        <div class="sb-sp-group" style="margin-top: 24px; margin-bottom: 24px;">
            <h3 class="sb-sp-heading"><?php esc_html_e('Clean Up Revisions', 'wpswitchboard'); ?></h3>
            <p class="sb-sp-description">
                <?php
                printf(
                    // translators: %s is the formatted number of revisions in the database
                    esc_html__('You currently have %s revisions in your database.', 'wpswitchboard'),
                    '<strong>' . number_format($revision_count) . '</strong>'
                );
                if ($posts_over_limit > 0 && $limit > 0) {
                    echo ' ';
                    printf(
                        // translators: %1$s is the number of posts over limit, %2$d is the revision limit, %3$s is the number of excess revisions
                        esc_html__('%1$s posts have more than %2$d revisions (%3$s excess can be removed now).', 'wpswitchboard'),
                        '<strong>' . number_format($posts_over_limit) . '</strong>',
                        esc_html($limit),
                        '<strong>' . number_format($excess_revisions) . '</strong>'
                    );
                } elseif ($limit > 0 && $revision_count > 0) {
                    echo ' ';
                    esc_html_e('All posts are within the limit.', 'wpswitchboard');
                }
                ?>
            </p>
            <?php if ($revision_count > 0): ?>
            <div class="sb-sp-button-group" style="justify-content: flex-start; margin-top: 16px; gap: 12px;">
                <?php if ($limit > 0): ?>
                <button type="button" class="<?php echo $posts_over_limit > 0 ? 'sb-module-button sb-module-button-primary sb-module-button-sm' : 'sb-module-button-sm'; ?>" id="enforce-limit-btn" <?php echo $posts_over_limit === 0 ? 'disabled' : ''; ?>><?php
                // translators: %d is the revision limit per post
                printf(esc_html__('Trim Now (Keep %d per post)', 'wpswitchboard'), esc_html($limit)); ?></button>
                <?php endif; ?>
                <button type="button" class="sb-module-button sb-module-button-secondary sb-module-button-sm" id="clean-revisions-btn"><?php esc_html_e('Delete All Revisions', 'wpswitchboard'); ?></button>
            </div>
            <?php else: ?>
            <p class="sb-sp-description" style="margin-top: 12px; color: #16a34a;">Done: <?php esc_html_e('No revisions to clean up.', 'wpswitchboard'); ?></p>
            <?php endif; ?>
        </div>

        <script>
        (function() {
            const enforceBtn = document.getElementById('enforce-limit-btn');
            const cleanBtn = document.getElementById('clean-revisions-btn');
            const limit = <?php echo esc_attr($limit); ?>;
            const nonce = '<?php echo esc_attr(wp_create_nonce('switchboard_revisions')); ?>';

            if (enforceBtn) {
                enforceBtn.addEventListener('click', function() {
                    if (!confirm('<?php echo esc_js(__('This will trim each post to keep only the most recent revisions (up to your limit). Continue?', 'wpswitchboard')); ?>')) return;

                    enforceBtn.disabled = true;
                    enforceBtn.textContent = '<?php echo esc_js(__('Processing...', 'wpswitchboard')); ?>';

                    fetch(ajaxurl, {
                        method: 'POST',
                        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                        body: 'action=switchboard_enforce_revision_limit&nonce=' + nonce
                    })
                    .then(r => r.json())
                    .then(d => {
                        alert(d.data.message);
                        location.reload();
                    })
                    .catch(() => {
                        enforceBtn.disabled = false;
                        enforceBtn.textContent = '<?php
                        // translators: %d is the revision limit per post
                        echo esc_js(sprintf(__('Trim Now (Keep %d per post)', 'wpswitchboard'), $limit)); ?>';
                    });
                });
            }

            cleanBtn.addEventListener('click', function() {
                if (!confirm('<?php echo esc_js(__('Are you sure? This will permanently delete ALL post revisions.', 'wpswitchboard')); ?>')) return;

                cleanBtn.disabled = true;
                cleanBtn.textContent = '<?php echo esc_js(__('Deleting...', 'wpswitchboard')); ?>';

                fetch(ajaxurl, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
                    body: 'action=switchboard_clean_revisions&nonce=' + nonce
                })
                .then(r => r.json())
                .then(d => {
                    alert(d.data.message);
                    location.reload();
                })
                .catch(() => {
                    cleanBtn.disabled = false;
                    cleanBtn.textContent = '<?php echo esc_js(__('Delete All Revisions', 'wpswitchboard')); ?>';
                });
            });
        })();
        </script>

        <div class="sb-sp-info-box sb-sp-info-box-info">
            <h4 class="sb-sp-heading-sm"><?php esc_html_e('How It Works', 'wpswitchboard'); ?></h4>
            <p class="sb-sp-description" style="margin: 0;">
                <?php esc_html_e('Excess revisions are automatically deleted whenever a post is saved. Use "Trim Now" to clean up existing posts immediately, or "Delete All" to remove everything.', 'wpswitchboard'); ?>
            </p>
        </div>
        <?php
    }

    public function set_revision_limit() {
        $limit = isset($this->settings['limit']) ? intval($this->settings['limit']) : 5;

        if ($limit === 0) {
            // Disable revisions
            if (!defined('WP_POST_REVISIONS')) {
                define('WP_POST_REVISIONS', false);
            }
        } elseif ($limit > 0) {
            // Set specific limit
            if (!defined('WP_POST_REVISIONS')) {
                define('WP_POST_REVISIONS', $limit);
            }
        }
        // If limit is negative, use default WordPress behavior (unlimited)
    }

    /**
     * Auto-enforce revision limit when a post is saved
     */
    public function auto_enforce_limit_on_save($post_id) {
        $limit = isset($this->settings['limit']) ? intval($this->settings['limit']) : 5;

        if ($limit <= 0) {
            return;
        }

        global $wpdb;

        // Get all revisions for this post, ordered by date (newest first)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Fetching revision IDs for trimming
        $revisions = $wpdb->get_col($wpdb->prepare("
            SELECT ID
            FROM $wpdb->posts
            WHERE post_type = 'revision' AND post_parent = %d
            ORDER BY post_date DESC
        ", $post_id));

        // If more revisions than limit, delete the excess (older ones)
        if (count($revisions) > $limit) {
            $revisions_to_delete = array_slice($revisions, $limit);

            foreach ($revisions_to_delete as $revision_id) {
                wp_delete_post_revision($revision_id);
            }
        }
    }

    /**
     * Enforce revision limit on all existing posts
     */
    public function ajax_enforce_limit() {
        check_ajax_referer('switchboard_revisions', 'nonce');

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

        $limit = isset($this->settings['limit']) ? intval($this->settings['limit']) : 5;

        if ($limit <= 0) {
            wp_send_json_error(['message' => esc_html__('Limit must be greater than 0 to enforce', 'wpswitchboard')]);
            return;
        }

        global $wpdb;

        // Get all posts that have revisions
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk revision enforcement query
        $posts_with_revisions = $wpdb->get_col("
            SELECT DISTINCT post_parent
            FROM $wpdb->posts
            WHERE post_type = 'revision' AND post_parent > 0
        ");

        $deleted_count = 0;
        $posts_trimmed = 0;

        foreach ($posts_with_revisions as $post_id) {
            // Get all revisions for this post, ordered by date (newest first)
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Per-post revision query in bulk operation
            $revisions = $wpdb->get_col($wpdb->prepare("
                SELECT ID
                FROM $wpdb->posts
                WHERE post_type = 'revision' AND post_parent = %d
                ORDER BY post_date DESC
            ", $post_id));

            // If more revisions than limit, delete the excess (older ones)
            if (count($revisions) > $limit) {
                $posts_trimmed++;
                $revisions_to_delete = array_slice($revisions, $limit);

                foreach ($revisions_to_delete as $revision_id) {
                    wp_delete_post_revision($revision_id);
                    $deleted_count++;
                }
            }
        }

        wp_send_json_success([
            'message' => sprintf(
                // translators: %1$d is the number of posts trimmed, %2$d is the number of deleted revisions, %3$d is the revision limit
                esc_html__('Trimmed %1$d posts, deleted %2$d excess revisions. Each post now has at most %3$d revisions.', 'wpswitchboard'),
                $posts_trimmed,
                $deleted_count,
                $limit
            )
        ]);
    }

    /**
     * Delete all revisions
     */
    public function ajax_clean_revisions() {
        check_ajax_referer('switchboard_revisions', 'nonce');

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

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Purging all revisions
        $revisions = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT ID FROM $wpdb->posts WHERE post_type = %s",
                'revision'
            )
        );

        $count = 0;
        if (is_array($revisions)) {
            foreach ($revisions as $revision) {
                if (isset($revision->ID)) {
                    wp_delete_post_revision($revision->ID);
                    $count++;
                }
            }
        }

        // translators: %d is the number of deleted post revisions
        wp_send_json_success(['message' => sprintf(esc_html__('Deleted %d post revisions.', 'wpswitchboard'), $count)]);
    }
}

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