<?php
/**
 * TamRank Migration Rollback
 *
 * Restores all post meta values from backup data
 * stored during migration. Runs in batches.
 */

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

class TamRank_Rollback {

    /** @var int Max posts per batch */
    const BATCH_SIZE = 50;

    public function __construct() {
        // run_batch() reads TamRank_Migration_Manager::STATE_OPTION, but the
        // rollback AJAX request never loads the manager class — a latent
        // fatal on the done-branch, exposed by the first real browser
        // rollback. Mirror the manager's own require_once pattern.
        if ( ! class_exists( 'TamRank_Migration_Manager' ) ) {
            require_once TAM_RANK_PLUGIN_PATH . 'includes/onboarding/class-tamrank-migration-manager.php';
        }
        if ( ! class_exists( 'TamRank_Option_Backup' ) ) {
            require_once TAM_RANK_PLUGIN_PATH . 'includes/onboarding/class-tamrank-option-backup.php';
        }
        // run_batch() clears the settings report by its class constant.
        if ( ! class_exists( 'TamRank_Settings_Migrator' ) ) {
            require_once TAM_RANK_PLUGIN_PATH . 'includes/onboarding/class-tamrank-settings-migrator.php';
        }
    }

    /**
     * Run a rollback batch.
     *
     * @param int $offset
     * @return array {
     *     @type int  $processed   Posts restored in this batch
     *     @type int  $next_offset Next offset
     *     @type int  $total       Total posts with backup data
     *     @type bool $done        Whether rollback is complete
     * }
     */
    public function run_batch( $offset = 0 ) {
        if ( function_exists( 'set_time_limit' ) ) {
            @set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged -- best-effort execution-time raise for batch processing; degrades gracefully if disabled
        }

        global $wpdb;

        // Redirects created by the migration are rolled back once, on the
        // first batch, by deleting exactly the row IDs the migration
        // recorded. User-created redirects are untouched (duplicates were
        // skipped during import, so no user row ID is ever in this list).
        if ( $offset === 0 ) {
            $redirect_ids = get_option( 'tamrank_migration_redirect_ids', array() );
            if ( is_array( $redirect_ids ) && ! empty( $redirect_ids ) ) {
                $redirects_table = $wpdb->prefix . 'tam_rank_redirects';
                foreach ( array_map( 'absint', $redirect_ids ) as $redirect_id ) {
                    if ( $redirect_id > 0 ) {
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- delete on a custom plugin table (tam_rank_redirects) during admin-context rollback; caching not applicable
                        $wpdb->delete( $redirects_table, array( 'id' => $redirect_id ), array( '%d' ) );
                    }
                }
            }
            delete_option( 'tamrank_migration_redirect_ids' );

            // Everything else the migration left in the options table. A rollback
            // that leaves bookkeeping behind is not a rollback: the settings
            // report described a migration that no longer exists (and nothing
            // reads it), and the dismissal of the "migration complete" notice
            // referred to that same vanished migration - keeping it would hide
            // the notice after a later, real migration.
            delete_option( TamRank_Settings_Migrator::REPORT_OPTION );
            delete_option( 'tamrank_migration_banner_dismissed' );

            // Site-wide options next, deliberately before the post loop. These are two
            // queries while the post loop is the long one, so if the rollback dies
            // halfway the site-wide configuration is at least already correct. The
            // restore writes raw (see TamRank_Option_Backup::restore) and flushes the
            // sitemap transients itself.
            TamRank_Option_Backup::restore();
        }

        // Get total count
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- direct query on a custom plugin table ({$wpdb->postmeta}); only the table identifier is interpolated, no user values; admin-context read, caching not applicable
        $total = (int) $wpdb->get_var(
            "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta}
             WHERE meta_key = '_tamrank_migration_backup'"
        );

        // Get batch of post IDs with backup data. Always read from the START:
        // the loop below deletes each processed row's backup marker, which
        // shrinks the result set this query paginates over — advancing the
        // caller-supplied offset here would skip half of the remaining posts
        // (batch 2 at offset 50 skips rows 51-100 of what is LEFT) and then
        // falsely report the rollback complete. The set simply drains to
        // empty; $offset is only used for the one-time redirect cleanup above
        // and echoed back as a progress counter.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- direct query on a custom plugin table ({$wpdb->postmeta}); only the table identifier is interpolated, user values use prepare() placeholders; admin-context read, caching not applicable
        $post_ids = $wpdb->get_col( $wpdb->prepare(
            "SELECT DISTINCT post_id FROM {$wpdb->postmeta}
             WHERE meta_key = '_tamrank_migration_backup'
             ORDER BY post_id ASC
             LIMIT %d",
            self::BATCH_SIZE
        ) );

        if ( empty( $post_ids ) ) {
            // Cleanup migration state
            delete_option( TamRank_Migration_Manager::STATE_OPTION );
            return array(
                'processed'   => 0,
                'next_offset' => $offset,
                'total'       => $total,
                'done'        => true,
            );
        }

        $processed = 0;

        foreach ( $post_ids as $post_id ) {
            $backup = get_post_meta( $post_id, '_tamrank_migration_backup', true );

            if ( ! is_array( $backup ) ) {
                // No valid backup — just remove the marker
                delete_post_meta( $post_id, '_tamrank_migration_backup' );
                $processed++;
                continue;
            }

            // Restore each meta key to its original value
            foreach ( $backup as $meta_key => $original_value ) {
                if ( empty( $original_value ) ) {
                    // Original was empty → remove the migrated value
                    delete_post_meta( $post_id, $meta_key );
                } else {
                    // Restore original value
                    update_post_meta( $post_id, $meta_key, $original_value );
                }
            }

            // Remove backup marker
            delete_post_meta( $post_id, '_tamrank_migration_backup' );
            $processed++;
        }

        $next_offset = $offset + self::BATCH_SIZE;
        $done = count( $post_ids ) < self::BATCH_SIZE;

        if ( $done ) {
            delete_option( TamRank_Migration_Manager::STATE_OPTION );
        }

        return array(
            'processed'   => $processed,
            'next_offset' => $next_offset,
            'total'       => $total,
            'done'        => $done,
        );
    }

}
