<?php

if ( ! defined( 'ABSPATH' ) ) { exit; }

class AIEO_DMM_As_Cleanup extends AIEO_DMM_Module_Base {

    const AJAX_ACTION       = 'aieo_dmm_as_cleanup_step';
    const NONCE_ACTION      = 'aieo_dmm_as_cleanup';
    const FORM_NONCE        = 'aieo_dmm_as_cleanup_form';
    const BYPASS_OPT        = 'aieo_as_bypass_all_optimizations';
    const BYPASS_OPT_LEGACY = 'om_as_bypass_all_optimizations';
    const AJAX_PROT_OPT     = 'aieo_as_ajax_protection_enabled';

    public function get_id()       { return 'as_cleanup'; }
    public function get_label()    { return __( 'Action Scheduler Cleanup', 'ai-eshop-optimizer' ); }
    public function get_group()    { return 'tools'; }
    public function get_priority() { return 60; }

    public function is_pro_locked() { return true; }
    public function get_pro_promo() {
        return array(
            'title'       => __( 'Action Scheduler Cleanup', 'ai-eshop-optimizer' ),
            'description' => __( 'Active maintenance for Action Scheduler — purges stale runs, defragments the queue, and prevents the slow accumulation of completed jobs that drags every WooCommerce cron tick once the table crosses a million rows.', 'ai-eshop-optimizer' ),
            'bullets'     => array(
                __( 'Chunked purge — never times out, never locks the queue.', 'ai-eshop-optimizer' ),
                __( 'AJAX-flood protection that throttles runaway action handlers before they take the site down.', 'ai-eshop-optimizer' ),
                __( 'Drop-in maintenance you can leave running — no manual intervention needed.', 'ai-eshop-optimizer' ),
            ),
        );
    }

    public function register_hooks() {
        add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'ajax_cleanup_step' ) );
        add_action( 'admin_notices',               array( $this, 'maybe_emit_bypass_notice' ) );
    }

    
    
    private function read_bypass_setting() {
        $val = get_option( self::BYPASS_OPT, null );
        if ( $val !== null ) { return (bool) $val; }
        return (bool) get_option( self::BYPASS_OPT_LEGACY, false );
    }

    public function maybe_emit_bypass_notice() {
        if ( ! current_user_can( 'manage_options' ) ) { return; }
        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
        if ( ! $screen ) { return; }
        
        $allowed = array( 'dashboard', 'plugins' );
        if ( ! in_array( $screen->id, $allowed, true ) ) { return; }
        if ( ! $this->read_bypass_setting() ) { return; }
        printf(
            '<div class="notice notice-error"><p><strong>%s</strong> %s</p><p><a href="%s">%s</a></p></div>',
            esc_html__( 'AS BYPASS MODE ACTIVE', 'ai-eshop-optimizer' ),
            esc_html__( '— All Action Scheduler optimizations are DISABLED. Testing only!', 'ai-eshop-optimizer' ),
            esc_url( $this->get_url() ),
            esc_html__( 'Manage AS settings', 'ai-eshop-optimizer' )
        );
    }

    public function render_subtab() {
        if ( ! current_user_can( 'manage_options' ) ) { return; }

        
        $form_message = '';
        if ( $_SERVER['REQUEST_METHOD'] === 'POST' 
            && isset( $_POST['aieo_dmm_as_cleanup_form_nonce'] )
            && wp_verify_nonce( wp_unslash( $_POST['aieo_dmm_as_cleanup_form_nonce'] ), self::FORM_NONCE ) ) {
            if ( isset( $_POST['aieo_dmm_as_toggle_bypass'] ) ) {
                $current = $this->read_bypass_setting();
                update_option( self::BYPASS_OPT, ! $current, false );
                
                
                delete_option( self::BYPASS_OPT_LEGACY );
                $form_message = ! $current
                    ? __( 'Bypass mode ENABLED — reload to apply.', 'ai-eshop-optimizer' )
                    : __( 'Bypass mode DISABLED — reload to apply.', 'ai-eshop-optimizer' );
            }
            if ( isset( $_POST['aieo_dmm_as_toggle_ajax_protection'] ) ) {
                $current = (bool) get_option( self::AJAX_PROT_OPT, true );
                update_option( self::AJAX_PROT_OPT, ! $current, true );
                $form_message = $current
                    ? __( 'AS AJAX query protection DISABLED — reload to apply. (Plugins that read AS state during AJAX may now slow down admin pages.)', 'ai-eshop-optimizer' )
                    : __( 'AS AJAX query protection ENABLED — reload to apply.', 'ai-eshop-optimizer' );
            }
        }

        $bypass_all     = $this->read_bypass_setting();
        $ajax_prot      = (bool) get_option( self::AJAX_PROT_OPT, true );

        
        global $wpdb;
        $table          = $wpdb->prefix . 'actionscheduler_actions';
        $stats          = array();
        $hook_breakdown = array();
        if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) {
            
            $stats          = $wpdb->get_results( "SELECT status, COUNT(*) as count FROM {$table} GROUP BY status", ARRAY_A );
            
            $hook_breakdown = $wpdb->get_results(
                "SELECT hook, COUNT(*) as total_pending, COUNT(DISTINCT args) as distinct_args
                 FROM {$table} WHERE status = 'pending'
                 GROUP BY hook ORDER BY total_pending DESC LIMIT 20",
                ARRAY_A
            );
        }

        $nonce = wp_create_nonce( self::NONCE_ACTION );

        echo '<div class="aieo-dmm-section">';
        echo '<h2>' . esc_html__( 'Action Scheduler Management', 'ai-eshop-optimizer' ) . '</h2>';
        echo '<p class="description">' . esc_html__( 'Diagnose and clean the Action Scheduler tables (wp_actionscheduler_actions/logs/claims/groups). Use the Deep Clean flow in steady state; the Nuclear flow only when blocked-hook actions have piled up.', 'ai-eshop-optimizer' ) . '</p>';

        if ( $form_message !== '' ) {
            echo '<div class="notice notice-warning inline"><p>' . esc_html( $form_message ) . '</p></div>';
        }

        
        echo '<div class="aieo-dmm-card">';
        echo '<h3 style="margin-top:0;color:' . ( $bypass_all ? '#c62828' : '#1565c0' ) . ';">';
        echo esc_html( $bypass_all
            ? __( 'BYPASS MODE ACTIVE', 'ai-eshop-optimizer' )
            : __( 'Optimizations Active', 'ai-eshop-optimizer' ) );
        echo '</h3>';
        if ( $bypass_all ) {
            echo '<p><strong>' . esc_html__( 'All AS optimizations are DISABLED.', 'ai-eshop-optimizer' ) . '</strong> '
                . esc_html__( 'Action Scheduler runs without any intervention.', 'ai-eshop-optimizer' ) . '</p>';
        } else {
            echo '<p>' . esc_html__( 'AIEO optimizations are protecting the site from memory issues and slow queries.', 'ai-eshop-optimizer' ) . '</p>';
        }
        echo '<form method="post" style="margin-top:10px;">';
        wp_nonce_field( self::FORM_NONCE, 'aieo_dmm_as_cleanup_form_nonce' );
        $btn_bg = $bypass_all ? '#4caf50' : '#ff9800';
        $btn_bc = $bypass_all ? '#388e3c' : '#f57c00';
        echo '<button type="submit" name="aieo_dmm_as_toggle_bypass" class="button" style="background:' . esc_attr( $btn_bg ) . ';border-color:' . esc_attr( $btn_bc ) . ';color:#fff;">';
        echo esc_html( $bypass_all
            ? __( 'Disable Bypass (Re-enable Optimizations)', 'ai-eshop-optimizer' )
            : __( 'Enable Bypass Mode (For Testing)', 'ai-eshop-optimizer' ) );
        echo '</button>';
        echo '</form>';
        echo '</div>';

        
        echo '<div class="aieo-dmm-card">';
        echo '<h3 style="margin-top:0;color:' . ( $ajax_prot ? '#1565c0' : '#c62828' ) . ';">';
        echo esc_html( $ajax_prot
            ? __( 'AS AJAX Query Protection: ACTIVE', 'ai-eshop-optimizer' )
            : __( 'AS AJAX Query Protection: DISABLED', 'ai-eshop-optimizer' ) );
        echo '</h3>';
        if ( $ajax_prot ) {
            echo '<p>' . esc_html__( 'Short-circuits Action Scheduler SELECT queries on admin AJAX so badly-behaved plugins (AIOSEO, AutomateWoo, WC Admin, etc.) can\'t turn a single AJAX call into a 90s+ delay. Scheduling and processing via cron/CLI are untouched.', 'ai-eshop-optimizer' ) . '</p>';
            echo '<p style="font-size:12px;color:#6b7280;">' . esc_html__( 'Disable this only if you\'re debugging an AS-related issue or if a custom plugin genuinely needs to read AS state from admin AJAX.', 'ai-eshop-optimizer' ) . '</p>';
        } else {
            echo '<p><strong>' . esc_html__( 'AS protection is OFF.', 'ai-eshop-optimizer' ) . '</strong> '
                . esc_html__( 'Every admin AJAX request can now run heavy Action Scheduler queries. Expect slow admin pages until you re-enable this.', 'ai-eshop-optimizer' ) . '</p>';
        }
        echo '<form method="post" style="margin-top:10px;">';
        wp_nonce_field( self::FORM_NONCE, 'aieo_dmm_as_cleanup_form_nonce' );
        $prot_bg = $ajax_prot ? '#ff9800' : '#4caf50';
        $prot_bc = $ajax_prot ? '#f57c00' : '#388e3c';
        echo '<button type="submit" name="aieo_dmm_as_toggle_ajax_protection" class="button" style="background:' . esc_attr( $prot_bg ) . ';border-color:' . esc_attr( $prot_bc ) . ';color:#fff;">';
        echo esc_html( $ajax_prot
            ? __( 'Disable AS AJAX Query Protection', 'ai-eshop-optimizer' )
            : __( 'Re-enable AS AJAX Query Protection', 'ai-eshop-optimizer' ) );
        echo '</button>';
        echo '</form>';
        echo '</div>';

        
        echo '<div class="aieo-dmm-card" id="aieo-as-stats-section">';
        echo '<h3>' . esc_html__( 'Current Status', 'ai-eshop-optimizer' ) . '</h3>';
        echo '<table class="widefat striped" style="max-width:400px;">';
        echo '<thead><tr><th>' . esc_html__( 'Status', 'ai-eshop-optimizer' ) . '</th><th>' . esc_html__( 'Count', 'ai-eshop-optimizer' ) . '</th></tr></thead><tbody>';
        foreach ( $stats as $s ) {
            echo '<tr><td>' . esc_html( (string) $s['status'] ) . '</td><td>' . esc_html( number_format_i18n( (int) $s['count'] ) ) . '</td></tr>';
        }
        echo '</tbody></table>';

        echo '<h4 style="margin-top:18px;">' . esc_html__( 'Top 20 Pending Hooks', 'ai-eshop-optimizer' ) . '</h4>';
        echo '<table class="widefat striped" style="max-width:700px;">';
        echo '<thead><tr><th>' . esc_html__( 'Hook', 'ai-eshop-optimizer' ) . '</th><th>' . esc_html__( 'Pending', 'ai-eshop-optimizer' ) . '</th><th>' . esc_html__( 'Distinct Args', 'ai-eshop-optimizer' ) . '</th></tr></thead><tbody>';
        foreach ( $hook_breakdown as $row ) {
            $is_dup = (int) $row['total_pending'] > (int) $row['distinct_args'];
            echo '<tr' . ( $is_dup ? ' style="background:#fff3e0;"' : '' ) . '>';
            echo '<td style="font-family:monospace;font-size:11px;">' . esc_html( (string) $row['hook'] ) . '</td>';
            echo '<td>' . esc_html( number_format_i18n( (int) $row['total_pending'] ) ) . '</td>';
            echo '<td>' . esc_html( number_format_i18n( (int) $row['distinct_args'] ) );
            if ( $is_dup ) {
                echo ' <span style="color:#e65100;font-weight:bold;" title="' . esc_attr__( 'Duplicates detected: more pending than distinct args', 'ai-eshop-optimizer' ) . '">&#9888;</span>';
            }
            echo '</td></tr>';
        }
        echo '</tbody></table>';
        echo '</div>';

        
        echo '<div class="aieo-dmm-card" id="aieo-as-buttons">';
        echo '<h3>' . esc_html__( 'Cleanup Actions', 'ai-eshop-optimizer' ) . '</h3>';

        echo '<div style="background:#e3f2fd;border:2px solid #2196f3;padding:15px;margin-bottom:16px;border-radius:4px;">';
        echo '<h4 style="margin-top:0;color:#1565c0;">' . esc_html__( 'Recommended: Deep Clean', 'ai-eshop-optimizer' ) . '</h4>';
        echo '<p>' . esc_html__( 'One-click comprehensive cleanup with real-time progress. Purges ALL completed/failed/canceled actions, deduplicates ALL pending hooks (keeps 1 newest per job), cleans orphaned logs, stale claims, and empty groups.', 'ai-eshop-optimizer' ) . '</p>';
        echo '<p style="font-size:12px;color:#666;">' . esc_html__( 'Processes 50,000 actions per batch. Safe for tables with millions of rows.', 'ai-eshop-optimizer' ) . '</p>';
        echo '<button type="button" class="button button-primary button-hero" onclick="aieoAsStartFlow(\'deep_clean\')">' . esc_html__( 'Deep Clean (All-in-One)', 'ai-eshop-optimizer' ) . '</button>';
        echo '</div>';

        echo '<h4>' . esc_html__( 'Individual Actions', 'ai-eshop-optimizer' ) . '</h4>';
        echo '<p><button type="button" class="button" onclick="aieoAsStartFlow(\'purge_finished\')">' . esc_html__( 'Purge Finished Actions (all completed/failed/canceled)', 'ai-eshop-optimizer' ) . '</button></p>';
        echo '<p><button type="button" class="button" onclick="aieoAsStartFlow(\'deduplicate\')">' . esc_html__( 'Deduplicate ALL Pending Hooks (keep 1 newest per job)', 'ai-eshop-optimizer' ) . '</button></p>';
        echo '<p><button type="button" class="button" style="background:#f44336;border-color:#c62828;color:#fff;" onclick="aieoAsStartFlow(\'nuclear\')">' . esc_html__( 'NUCLEAR: Wipe Blocked Hooks + All Finished', 'ai-eshop-optimizer' ) . '</button></p>';
        echo '</div>';

        
        echo '<div class="aieo-dmm-card" id="aieo-as-progress" style="display:none;">';
        echo '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">';
        echo '<h3 style="margin:0;" id="aieo-as-flow-title">' . esc_html__( 'Cleanup in Progress', 'ai-eshop-optimizer' ) . '</h3>';
        echo '<div><span id="aieo-as-elapsed" style="font-family:monospace;margin-right:15px;font-size:14px;">0m 00s</span>';
        echo '<button type="button" class="button" id="aieo-as-stop-btn" style="background:#f44336;border-color:#c62828;color:#fff;">' . esc_html__( 'Stop', 'ai-eshop-optimizer' ) . '</button></div>';
        echo '</div>';
        echo '<div style="background:#e0e0e0;border-radius:4px;height:26px;margin-bottom:15px;overflow:hidden;position:relative;">';
        echo '<div id="aieo-as-bar" style="background:linear-gradient(90deg,#1976d2,#42a5f5);height:100%;border-radius:4px;transition:width .3s;width:0%;"></div>';
        echo '<span id="aieo-as-pct" style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font-weight:bold;font-size:12px;color:#333;text-shadow:0 0 3px #fff;">0%</span>';
        echo '</div>';
        echo '<div id="aieo-as-steps" style="font-size:13px;line-height:2.0;"></div>';
        echo '<div id="aieo-as-error"   style="display:none;margin-top:15px;"></div>';
        echo '<div id="aieo-as-summary" style="margin-top:15px;"></div>';
        echo '</div>';

        
        echo '<div class="aieo-dmm-card">';
        echo '<h3>' . esc_html__( 'WP-CLI Commands', 'ai-eshop-optimizer' ) . '</h3>';
        echo '<pre style="background:#f5f5f5;padding:10px;overflow-x:auto;font-size:11px;margin:0;">';
        echo "# Delete failed/canceled actions\n";
        echo "wp db query \"DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE status IN ('failed','canceled') LIMIT 50000\"\n\n"; 
        echo "# Delete old completed actions\n";
        echo "wp db query \"DELETE FROM {$wpdb->prefix}actionscheduler_actions WHERE status = 'complete' LIMIT 50000\"\n\n"; 
        echo "# Clean orphaned logs\n";
        echo "wp db query \"DELETE FROM {$wpdb->prefix}actionscheduler_logs WHERE action_id NOT IN (SELECT action_id FROM {$wpdb->prefix}actionscheduler_actions) LIMIT 100000\""; 
        echo '</pre>';
        echo '</div>';

        echo '</div>'; 

        $this->print_inline_js( $nonce );
    }

    
    private function print_inline_js( $nonce ) {
        $ajax_url = admin_url( 'admin-ajax.php' );
        ?>
        <script>
        (function($) {
            'use strict';

            var NONCE    = <?php echo wp_json_encode( $nonce ); ?>;
            var AJAX_URL = <?php echo wp_json_encode( $ajax_url ); ?>;
            var ACTION   = <?php echo wp_json_encode( self::AJAX_ACTION ); ?>;

            var FLOWS = {
                deep_clean: {
                    title: 'Deep Clean',
                    confirm: 'Run Deep Clean?\n\n'
                        + '1. Delete ALL completed/failed/canceled actions\n'
                        + '2. Deduplicate ALL pending hooks (keep 1 per job)\n'
                        + '3. Enforce singleton hooks (keep 1 per hook regardless of args)\n'
                        + '4. Clean orphaned logs, stale claims, empty groups\n\n'
                        + 'Safe but irreversible. Continue?',
                    steps: [
                        { step:'stats',           label:'Getting initial counts' },
                        { step:'purge', data:{status:'complete'},  label:'Purging completed actions',  batch:true, statKey:'complete' },
                        { step:'purge', data:{status:'failed'},    label:'Purging failed actions',     batch:true, statKey:'failed' },
                        { step:'purge', data:{status:'canceled'},  label:'Purging canceled actions',   batch:true, statKey:'canceled' },
                        { step:'deduplicate',     label:'Deduplicating pending hooks (same args)' },
                        { step:'deduplicate_singletons', label:'Enforcing singleton hooks (1 per hook)' },
                        { step:'orphan_logs',     label:'Cleaning orphaned logs',     batch:true, statKey:'logs' },
                        { step:'truncate_claims', label:'Clearing stale claims' },
                        { step:'clean_groups',    label:'Removing empty groups' },
                        { step:'stats',           label:'Getting final counts' }
                    ]
                },
                purge_finished: {
                    title: 'Purge Finished Actions',
                    confirm: 'Purge ALL completed, failed, and canceled actions?\n\nSafe but irreversible. Continue?',
                    steps: [
                        { step:'stats',           label:'Getting initial counts' },
                        { step:'purge', data:{status:'complete'},  label:'Purging completed actions',  batch:true, statKey:'complete' },
                        { step:'purge', data:{status:'failed'},    label:'Purging failed actions',     batch:true, statKey:'failed' },
                        { step:'purge', data:{status:'canceled'},  label:'Purging canceled actions',   batch:true, statKey:'canceled' },
                        { step:'orphan_logs',     label:'Cleaning orphaned logs',     batch:true, statKey:'logs' },
                        { step:'truncate_claims', label:'Clearing stale claims' },
                        { step:'clean_groups',    label:'Removing empty groups' },
                        { step:'stats',           label:'Getting final counts' }
                    ]
                },
                deduplicate: {
                    title: 'Deduplicate Pending',
                    confirm: 'Deduplicate ALL pending hooks?\nKeeps only 1 newest action per job.\nAlso enforces singleton hooks (1 per hook).\n\nSafe but irreversible. Continue?',
                    steps: [
                        { step:'stats',           label:'Getting initial counts' },
                        { step:'deduplicate',     label:'Deduplicating pending hooks (same args)' },
                        { step:'deduplicate_singletons', label:'Enforcing singleton hooks (1 per hook)' },
                        { step:'orphan_logs',     label:'Cleaning orphaned logs',     batch:true, statKey:'logs' },
                        { step:'clean_groups',    label:'Removing empty groups' },
                        { step:'stats',           label:'Getting final counts' }
                    ]
                },
                nuclear: {
                    title: 'NUCLEAR Cleanup',
                    confirm: 'NUCLEAR CLEANUP!\n\n'
                        + '1. Delete ALL actions for blocked hooks (including pending!)\n'
                        + '2. Delete ALL completed/failed/canceled actions\n'
                        + '3. Deduplicate remaining pending hooks\n'
                        + '4. Clean orphaned logs, stale claims, empty groups\n\n'
                        + 'AGGRESSIVE and irreversible. Continue?',
                    steps: [
                        { step:'stats',           label:'Getting initial counts' },
                        { step:'nuclear_hooks',   label:'Wiping blocked hooks',       batch:true },
                        { step:'purge', data:{status:'complete'},  label:'Purging completed actions',  batch:true, statKey:'complete' },
                        { step:'purge', data:{status:'failed'},    label:'Purging failed actions',     batch:true, statKey:'failed' },
                        { step:'purge', data:{status:'canceled'},  label:'Purging canceled actions',   batch:true, statKey:'canceled' },
                        { step:'deduplicate',     label:'Deduplicating pending hooks (same args)' },
                        { step:'deduplicate_singletons', label:'Enforcing singleton hooks (1 per hook)' },
                        { step:'orphan_logs',     label:'Cleaning orphaned logs',     batch:true, statKey:'logs' },
                        { step:'truncate_claims', label:'Clearing stale claims' },
                        { step:'clean_groups',    label:'Removing empty groups' },
                        { step:'stats',           label:'Getting final counts' }
                    ]
                }
            };

            var flow=null, stepIdx=0, stopped=false, startTime=null, timerHandle=null;
            var initialStats=null, finalStats=null, stepTotals={}, stepExtras={};

            function fmt(n) { return parseInt(n || 0).toLocaleString(); }
            function elapsedText() {
                if (!startTime) return '0m 00s';
                var s = Math.round((Date.now() - startTime) / 1000);
                var m = Math.floor(s / 60); s = s % 60;
                return m + 'm ' + (s < 10 ? '0' : '') + s + 's';
            }

            function render() {
                if (!flow) return;
                var html = '';
                for (var i = 0; i < flow.steps.length; i++) {
                    var s = flow.steps[i];
                    var isDone = (i < stepIdx);
                    var isRun  = (i === stepIdx && !stopped);
                    var icon, color;
                    if (isDone)      { icon = '✔'; color = '#2e7d32'; }
                    else if (isRun)  { icon = '⏳'; color = '#1565c0'; }
                    else             { icon = '○'; color = '#999';    }
                    var detail = '';
                    if (stepTotals[i] !== undefined && stepTotals[i] > 0) {
                        var target = '';
                        if (s.statKey && initialStats && initialStats[s.statKey]) {
                            target = ' / ~' + fmt(initialStats[s.statKey]);
                        }
                        detail = ' — ' + fmt(stepTotals[i]) + target + ' deleted';
                    }
                    if ((s.step === 'deduplicate' || s.step === 'deduplicate_singletons') && stepExtras[i]) {
                        detail = ' — ' + fmt(stepTotals[i] || 0) + ' dupes removed across ' + fmt(stepExtras[i].hooks_cleaned) + ' hooks';
                    }
                    if (s.step === 'stats' && i === 0 && initialStats) {
                        detail = ' — ' + fmt(initialStats.total) + ' actions, ' + fmt(initialStats.logs) + ' logs, ' + fmt(initialStats.claims) + ' claims, ' + fmt(initialStats.groups) + ' groups';
                    }
                    if (s.step === 'stats' && i > 0 && finalStats) {
                        detail = ' — ' + fmt(finalStats.total) + ' actions remaining';
                    }
                    html += '<div style="color:' + color + ';padding:2px 0;">'
                         +  '<span style="display:inline-block;width:22px;text-align:center;">' + icon + '</span> '
                         +  s.label + '<span style="color:#666;">' + detail + '</span>'
                         +  '</div>';
                }
                $('#aieo-as-steps').html(html);

                var total = flow.steps.length, partial = 0;
                if (stepIdx < total) {
                    var cur = flow.steps[stepIdx];
                    if (cur.batch && cur.statKey && initialStats) {
                        var tgt = initialStats[cur.statKey] || 0;
                        var del = stepTotals[stepIdx] || 0;
                        if (tgt > 0) partial = Math.min(1, del / tgt);
                    }
                }
                var pct = Math.min(100, Math.round(((stepIdx + partial) / total) * 100));
                $('#aieo-as-bar').css('width', pct + '%');
                $('#aieo-as-pct').text(pct + '%');
            }

            function startFlow(name) {
                if (!FLOWS[name]) return;
                if (!confirm(FLOWS[name].confirm)) return;
                flow = FLOWS[name]; stepIdx = 0; stopped = false;
                startTime = Date.now(); initialStats = null; finalStats = null;
                stepTotals = {}; stepExtras = {};

                $('#aieo-as-buttons').slideUp(200);
                $('#aieo-as-progress').slideDown(200);
                $('#aieo-as-flow-title').text(flow.title + ' in Progress');
                $('#aieo-as-error').hide().empty();
                $('#aieo-as-summary').empty();
                $('#aieo-as-stop-btn').text('Stop').prop('disabled', false).show();
                $('#aieo-as-bar').css('width', '0%');
                $('#aieo-as-pct').text('0%');

                timerHandle = setInterval(function() { $('#aieo-as-elapsed').text(elapsedText()); }, 1000);
                render();
                runStep(0);
            }

            function runStep(idx) {
                if (stopped) { onStopped(); return; }
                if (idx >= flow.steps.length) { onComplete(); return; }
                stepIdx = idx; render();
                var s = flow.steps[idx];
                var data = $.extend({ action: ACTION, nonce: NONCE, step: s.step }, s.data || {});
                $.ajax({
                    url: AJAX_URL, method: 'POST', data: data, timeout: 180000,
                    success: function(resp) {
                        if (stopped) { onStopped(); return; }
                        if (!resp.success) { onError('Step "' + s.label + '" failed: ' + (resp.data || 'Unknown error')); return; }
                        handleResult(idx, s, resp.data);
                    },
                    error: function(xhr, textStatus, err) {
                        onError('AJAX error on "' + s.label + '": ' + err + ' (' + textStatus + '). You can safely retry.');
                    }
                });
            }

            function handleResult(idx, s, data) {
                if (s.step === 'stats') {
                    if (!initialStats) {
                        initialStats = $.extend({total: data.total}, data.statuses);
                        initialStats.logs = data.logs; initialStats.claims = data.claims; initialStats.groups = data.groups;
                    } else {
                        finalStats = $.extend({total: data.total}, data.statuses);
                    }
                    stepIdx = idx + 1; render(); runStep(idx + 1); return;
                }
                if (s.batch) {
                    stepTotals[idx] = (stepTotals[idx] || 0) + (data.deleted || 0);
                    render();
                    if (!data.done && data.deleted > 0) { runStep(idx); return; }
                }
                if (!s.batch && data.deleted !== undefined) {
                    stepTotals[idx] = (stepTotals[idx] || 0) + data.deleted;
                }
                if (data.hooks_cleaned !== undefined) {
                    stepExtras[idx] = { hooks_cleaned: data.hooks_cleaned };
                }
                stepIdx = idx + 1; render(); runStep(idx + 1);
            }

            function onComplete() {
                clearInterval(timerHandle); stepIdx = flow.steps.length; render();
                var h = '<div class="notice notice-success" style="margin:0;"><p><strong>' + flow.title + ' completed!</strong></p>';
                if (initialStats && finalStats) {
                    var before = initialStats.total, after = finalStats.total;
                    var red = before > 0 ? Math.round(((before - after) / before) * 100) : 0;
                    h += '<p><strong>' + fmt(before) + '</strong> &rarr; <strong>' + fmt(after) + '</strong> actions (' + red + '% reduction)</p>';
                }
                h += '<ul style="margin:5px 0 10px 20px;">';
                for (var i = 0; i < flow.steps.length; i++) {
                    if (stepTotals[i] && stepTotals[i] > 0) {
                        var lbl = flow.steps[i].label, extra = '';
                        if (stepExtras[i] && stepExtras[i].hooks_cleaned) {
                            extra = ' (' + fmt(stepExtras[i].hooks_cleaned) + ' hooks)';
                        }
                        h += '<li>' + lbl + ': <strong>' + fmt(stepTotals[i]) + '</strong>' + extra + '</li>';
                    }
                }
                h += '</ul>';
                h += '<p>Elapsed: <strong>' + elapsedText() + '</strong></p>'
                  +  '<p style="margin-top:10px;"><a href="" class="button button-primary">Reload Page to See Updated Stats</a></p></div>';
                $('#aieo-as-summary').html(h);
                $('#aieo-as-stop-btn').hide();
            }

            function onStopped() {
                clearInterval(timerHandle);
                var h = '<div class="notice notice-warning" style="margin:0;"><p><strong>Cleanup stopped.</strong> All progress so far is saved.</p>'
                      + '<p style="margin-top:10px;"><a href="" class="button button-primary">Reload Page</a> '
                      + '<button type="button" class="button" id="aieo-as-back-btn">Back to Actions</button></p></div>';
                $('#aieo-as-summary').html(h);
                $('#aieo-as-stop-btn').hide();
            }

            function onError(msg) {
                clearInterval(timerHandle);
                var h = '<div class="notice notice-error" style="margin:0;"><p><strong>Error:</strong> ' + $('<span>').text(msg).html() + '</p>'
                      + '<p>All progress so far is saved. You can safely retry.</p>'
                      + '<p style="margin-top:10px;"><a href="" class="button button-primary">Reload Page</a> '
                      + '<button type="button" class="button" id="aieo-as-back-btn">Back to Actions</button></p></div>';
                $('#aieo-as-error').html(h).show();
                $('#aieo-as-stop-btn').hide();
            }

            $(document).on('click', '#aieo-as-stop-btn', function() {
                stopped = true;
                $(this).text('Stopping after current batch…').prop('disabled', true);
            });
            $(document).on('click', '#aieo-as-back-btn', function() {
                $('#aieo-as-progress').slideUp(200);
                $('#aieo-as-buttons').slideDown(200);
            });
            $(window).on('beforeunload', function() {
                if (flow && !stopped && stepIdx < flow.steps.length) {
                    return 'Cleanup is in progress. Are you sure you want to leave?';
                }
            });

            window.aieoAsStartFlow = startFlow;
        })(jQuery);
        </script>
        <?php
    }

    
    public function ajax_cleanup_step() {
        check_ajax_referer( self::NONCE_ACTION, 'nonce' );

        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized' );
        }

        @set_time_limit( 300 ); 

        global $wpdb;
        $table  = $wpdb->prefix . 'actionscheduler_actions';
        $logs   = $wpdb->prefix . 'actionscheduler_logs';
        $claims = $wpdb->prefix . 'actionscheduler_claims';
        $groups = $wpdb->prefix . 'actionscheduler_groups';

        $batch_size = 50000;
        $step = isset( $_POST['step'] ) ? sanitize_text_field( wp_unslash( $_POST['step'] ) ) : '';

        switch ( $step ) {

            case 'stats':
                $statuses = array();
                
                $rows = $wpdb->get_results( "SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status", ARRAY_A );
                foreach ( $rows as $r ) {
                    $statuses[ $r['status'] ] = (int) $r['cnt'];
                }
                $total = array_sum( $statuses );

                $log_count = 0; $claim_count = 0; $group_count = 0;
                if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $logs ) ) === $logs ) {
                    
                    $log_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$logs}" );
                }
                if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $claims ) ) === $claims ) {
                    
                    $claim_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$claims}" );
                }
                if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $groups ) ) === $groups ) {
                    
                    $group_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$groups}" );
                }

                
                $pending_hooks = $wpdb->get_results(
                    "SELECT hook, COUNT(*) as cnt, COUNT(DISTINCT args) as distinct_args
                     FROM {$table} WHERE status = 'pending'
                     GROUP BY hook ORDER BY cnt DESC LIMIT 20",
                    ARRAY_A
                );

                wp_send_json_success( array(
                    'statuses'      => $statuses,
                    'total'         => $total,
                    'logs'          => $log_count,
                    'claims'        => $claim_count,
                    'groups'        => $group_count,
                    'pending_hooks' => $pending_hooks,
                ) );
                break;

            case 'purge':
                $status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'complete';
                if ( ! in_array( $status, array( 'complete', 'failed', 'canceled' ), true ) ) {
                    wp_send_json_error( 'Invalid status' );
                }
                $deleted = (int) $wpdb->query( $wpdb->prepare(
                    
                    "DELETE FROM {$table} WHERE status = %s LIMIT %d",
                    $status,
                    $batch_size
                ) );
                wp_send_json_success( array(
                    'deleted' => $deleted,
                    'done'    => ( $deleted < $batch_size ),
                ) );
                break;

            case 'deduplicate':
                
                $dupes = $wpdb->get_results(
                    "SELECT hook, args, COUNT(*) as cnt, MAX(action_id) as keep_id
                     FROM {$table}
                     WHERE status = 'pending'
                     GROUP BY hook, args
                     HAVING cnt > 1"
                );
                $total_deleted = 0;
                $hooks_cleaned = 0;
                foreach ( $dupes as $dup ) {
                    $del = (int) $wpdb->query( $wpdb->prepare(
                        
                        "DELETE FROM {$table}
                         WHERE hook = %s AND args = %s AND status = 'pending' AND action_id != %d",
                        $dup->hook, $dup->args, $dup->keep_id
                    ) );
                    if ( $del > 0 ) {
                        $total_deleted += $del;
                        $hooks_cleaned++;
                    }
                }
                wp_send_json_success( array(
                    'deleted'       => $total_deleted,
                    'hooks_cleaned' => $hooks_cleaned,
                ) );
                break;

            case 'deduplicate_singletons':
                
                
                
                $singleton_hooks = defined( 'AIEO_AS_SINGLETON_HOOKS' )
                    ? AIEO_AS_SINGLETON_HOOKS
                    : ( defined( 'OM_AS_SINGLETON_HOOKS' ) ? OM_AS_SINGLETON_HOOKS : array() );
                if ( empty( $singleton_hooks ) ) {
                    wp_send_json_success( array( 'deleted' => 0, 'hooks_cleaned' => 0 ) );
                    break;
                }
                $total_deleted = 0;
                $hooks_cleaned = 0;
                foreach ( $singleton_hooks as $hook ) {
                    $keep_id = $wpdb->get_var( $wpdb->prepare(
                        
                        "SELECT MAX(action_id) FROM {$table} WHERE hook = %s AND status = 'pending'",
                        $hook
                    ) );
                    if ( ! $keep_id ) { continue; }
                    $del = (int) $wpdb->query( $wpdb->prepare(
                        
                        "DELETE FROM {$table} WHERE hook = %s AND status = 'pending' AND action_id != %d",
                        $hook, $keep_id
                    ) );
                    if ( $del > 0 ) {
                        $total_deleted += $del;
                        $hooks_cleaned++;
                    }
                }
                wp_send_json_success( array(
                    'deleted'       => $total_deleted,
                    'hooks_cleaned' => $hooks_cleaned,
                ) );
                break;

            case 'orphan_logs':
                if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $logs ) ) !== $logs ) {
                    wp_send_json_success( array( 'deleted' => 0, 'done' => true ) );
                    break;
                }
                $log_batch = 100000;
                $deleted = (int) $wpdb->query(
                    
                    "DELETE FROM {$logs}
                     WHERE action_id NOT IN (SELECT action_id FROM {$table})
                     LIMIT {$log_batch}"
                );
                wp_send_json_success( array(
                    'deleted' => $deleted,
                    'done'    => ( $deleted < $log_batch ),
                ) );
                break;

            case 'truncate_claims':
                $count = 0;
                if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $claims ) ) === $claims ) {
                    
                    $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$claims}" );
                    if ( $count > 0 ) {
                        
                        $wpdb->query( "TRUNCATE TABLE {$claims}" );
                    }
                }
                wp_send_json_success( array( 'deleted' => $count ) );
                break;

            case 'clean_groups':
                $deleted = 0;
                if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $groups ) ) === $groups ) {
                    $deleted = (int) $wpdb->query(
                        
                        "DELETE g FROM {$groups} g
                         LEFT JOIN {$table} a ON g.group_id = a.group_id
                         WHERE a.action_id IS NULL"
                    );
                }
                wp_send_json_success( array( 'deleted' => $deleted ) );
                break;

            case 'nuclear_hooks':
                $blocked_hooks    = defined( 'AIEO_AS_BLOCKED_HOOKS' )
                    ? AIEO_AS_BLOCKED_HOOKS
                    : ( defined( 'OM_AS_BLOCKED_HOOKS' )    ? OM_AS_BLOCKED_HOOKS    : array() );
                $blocked_patterns = defined( 'AIEO_AS_BLOCKED_PATTERNS' )
                    ? AIEO_AS_BLOCKED_PATTERNS
                    : ( defined( 'OM_AS_BLOCKED_PATTERNS' ) ? OM_AS_BLOCKED_PATTERNS : array() );
                $total    = 0;
                $all_done = true;
                foreach ( $blocked_hooks as $hook ) {
                    $del = (int) $wpdb->query( $wpdb->prepare(
                        
                        "DELETE FROM {$table} WHERE hook = %s LIMIT %d",
                        $hook, $batch_size
                    ) );
                    $total += $del;
                    if ( $del >= $batch_size ) { $all_done = false; }
                }
                foreach ( $blocked_patterns as $pattern ) {
                    $safe = $wpdb->esc_like( $pattern ) . '%';
                    $del  = (int) $wpdb->query( $wpdb->prepare(
                        
                        "DELETE FROM {$table} WHERE hook LIKE %s LIMIT %d",
                        $safe, $batch_size
                    ) );
                    $total += $del;
                    if ( $del >= $batch_size ) { $all_done = false; }
                }
                wp_send_json_success( array(
                    'deleted' => $total,
                    'done'    => $all_done,
                ) );
                break;

            default:
                wp_send_json_error( 'Unknown step: ' . $step );
        }
    }
}
