<?php
/**
 * Domain Changer Module
 *
 * Change your site domain and update all database references in one click.
 *
 * @package BlaminhorEssentials
 */

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

/**
 * Domain Changer module class
 */
class Blaminhor_Essentials_Module_Domain_Changer extends Blaminhor_Essentials_Module {

	/**
	 * Module ID
	 *
	 * @var string
	 */
	protected $id = 'domain-changer';

	/**
	 * Module name
	 *
	 * @var string
	 */
	protected $name = 'Domain Changer';

	/**
	 * Backup directory path
	 *
	 * @var string
	 */
	private $backup_dir;

	/**
	 * Initialize module
	 */
	protected function init() {
		$this->name        = __( 'Domain Changer', 'blaminhor-essentials' );
		$this->description = __( 'Change your site domain and update all database references in one click.', 'blaminhor-essentials' );
		$this->backup_dir  = is_multisite()
			? WP_CONTENT_DIR . '/blaminhor-essentials/backups/' . get_current_blog_id()
			: WP_CONTENT_DIR . '/blaminhor-essentials/backups';
	}

	/**
	 * Get default settings
	 *
	 * @return array
	 */
	protected function get_default_settings() {
		return array(
			'backup_before_change' => true,
			'last_change_date'     => '',
			'last_old_domain'      => '',
			'last_new_domain'      => '',
		);
	}

	/**
	 * Register hooks
	 */
	protected function register_hooks() {
		add_action( 'admin_init', array( $this, 'handle_settings_submission' ) );

		// AJAX handlers.
		add_action( 'wp_ajax_ap_domain_create_backup', array( $this, 'ajax_create_backup' ) );
		add_action( 'wp_ajax_ap_domain_change', array( $this, 'ajax_change_domain' ) );
		add_action( 'wp_ajax_ap_domain_preview', array( $this, 'ajax_preview_changes' ) );
	}

	/**
	 * Enqueue module assets
	 *
	 * @param string $hook Current admin page hook.
	 */
	public function enqueue_assets( $hook ) {
		if ( strpos( $hook, 'blaminhor-essentials-domain-changer' ) === false ) {
			return;
		}

		$config = array(
			'nonce'         => wp_create_nonce( 'blaminhor_essentials_admin' ),
			'ajaxurl'       => admin_url( 'admin-ajax.php' ),
			'currentDomain' => $this->get_current_domain(),
			'strings'       => array(
				'creatingBackup'      => __( 'Creating backup...', 'blaminhor-essentials' ),
				'backupCreated'       => __( 'Backup created:', 'blaminhor-essentials' ),
				'errorCreatingBackup' => __( 'Error creating backup.', 'blaminhor-essentials' ),
				'enterNewDomain'      => __( 'Please enter a new domain.', 'blaminhor-essentials' ),
				'enterSearchDomain'   => __( 'Please enter a search domain.', 'blaminhor-essentials' ),
				'sameDomains'         => __( 'The search domain and new domain must be different.', 'blaminhor-essentials' ),
				'loading'             => __( 'Loading...', 'blaminhor-essentials' ),
				'previewChanges'      => __( 'Preview Changes', 'blaminhor-essentials' ),
				'errorPreview'        => __( 'Error previewing changes.', 'blaminhor-essentials' ),
				'from'                => __( 'From:', 'blaminhor-essentials' ),
				'to'                  => __( 'To:', 'blaminhor-essentials' ),
				'affectedRows'        => __( 'Affected rows:', 'blaminhor-essentials' ),
				'confirmChange'       => __( 'Are you sure you want to change the domain? This action cannot be undone.', 'blaminhor-essentials' ),
				'changingDomain'      => __( 'Changing domain...', 'blaminhor-essentials' ),
				'changeDomainNow'     => __( 'Change Domain Now', 'blaminhor-essentials' ),
				'successRedirect'     => __( 'Domain changed successfully! You will be redirected to the new domain.', 'blaminhor-essentials' ),
				'errorChanging'       => __( 'Error changing domain.', 'blaminhor-essentials' ),
			),
		);

		wp_add_inline_script(
			'blaminhor-essentials-modules',
			'jQuery(document).ready(function($) {
				var DomainChanger = {
					config: ' . wp_json_encode( $config ) . ',
					backupCreated: false,
					previewDone: false,
					init: function() {
						this.bindEvents();
					},
					bindEvents: function() {
						var self = this;

						// Create backup
						$("#ap-domain-backup-btn").on("click", function() {
							var $btn = $(this);
							var originalText = $btn.html();

							$btn.prop("disabled", true).html(\'<span class="dashicons dashicons-update ap-spin"></span> \' + self.config.strings.creatingBackup);

							$.post(self.config.ajaxurl, {
								action: "ap_domain_create_backup",
								nonce: self.config.nonce
							}, function(response) {
								if (response.success) {
									self.backupCreated = true;
									$("#ap-domain-backup-status").html(
										\'<div class="blaminhor-essentials-notice success" style="margin: 0;">\' +
										\'<p><span class="dashicons dashicons-yes-alt" class="ap-text-success"></span> \' +
										self.config.strings.backupCreated + " <strong>" + response.data.filename + "</strong> (" + response.data.size + ")</p>" +
										"</div>"
									);
									self.updateChangeButton();
								} else {
									$btn.prop("disabled", false).html(originalText);
									alert(response.data || self.config.strings.errorCreatingBackup);
								}
							}).fail(function() {
								$btn.prop("disabled", false).html(originalText);
								alert(self.config.strings.errorCreatingBackup);
							});
						});

						// Old domain input change - show/hide diff notice.
						$("#ap-old-domain").on("input", function() {
							var oldDomain = $(this).val().trim();
							if (oldDomain && oldDomain !== self.config.currentDomain) {
								$("#ap-domain-diff-notice").show();
							} else {
								$("#ap-domain-diff-notice").hide();
							}
							self.previewDone = false;
							$("#ap-domain-preview").hide();
							self.updateChangeButton();
						});

						// Preview changes
						$("#ap-domain-preview-btn").on("click", function() {
							var oldDomain = $("#ap-old-domain").val().trim();
							var newDomain = $("#ap-new-domain").val().trim();

							if (!oldDomain) {
								alert(self.config.strings.enterSearchDomain);
								return;
							}
							if (!newDomain) {
								alert(self.config.strings.enterNewDomain);
								return;
							}
							if (oldDomain === newDomain) {
								alert(self.config.strings.sameDomains);
								return;
							}

							var $btn = $(this);
							$btn.prop("disabled", true).text(self.config.strings.loading);

							$.post(self.config.ajaxurl, {
								action: "ap_domain_preview",
								nonce: self.config.nonce,
								old_domain: oldDomain,
								new_domain: newDomain
							}, function(response) {
								$btn.prop("disabled", false).text(self.config.strings.previewChanges);

								if (response.success) {
									self.previewDone = true;
									var html = "<p><strong>" + self.config.strings.from + "</strong> " + response.data.old_domain + "<br>";
									html += "<strong>" + self.config.strings.to + "</strong> " + response.data.new_domain + "</p>";
									html += "<p><strong>" + self.config.strings.affectedRows + "</strong> " + response.data.total + "</p>";

									if (response.data.preview.length > 0) {
										html += \'<ul style="margin: 10px 0 0 20px;">\';
										response.data.preview.forEach(function(item) {
											html += "<li>" + item.label + "</li>";
										});
										html += "</ul>";
									}

									$("#ap-domain-preview-content").html(html);
									$("#ap-domain-preview").show();
									self.updateChangeButton();
								} else {
									alert(response.data || self.config.strings.errorPreview);
								}
							}).fail(function() {
								$btn.prop("disabled", false).text(self.config.strings.previewChanges);
								alert(self.config.strings.errorPreview);
							});
						});

						// Domain input change
						$("#ap-new-domain").on("input", function() {
							self.previewDone = false;
							$("#ap-domain-preview").hide();
							self.updateChangeButton();
						});

						// Execute domain change
						$("#ap-domain-change-btn").on("click", function() {
							var oldDomain = $("#ap-old-domain").val().trim();
							var newDomain = $("#ap-new-domain").val().trim();

							if (!oldDomain || !newDomain) {
								alert(self.config.strings.enterNewDomain);
								return;
							}

							if (!confirm(self.config.strings.confirmChange)) {
								return;
							}

							var $btn = $(this);
							$btn.prop("disabled", true).html(\'<span class="dashicons dashicons-update ap-spin"></span> \' + self.config.strings.changingDomain);

							$.post(self.config.ajaxurl, {
								action: "ap_domain_change",
								nonce: self.config.nonce,
								old_domain: oldDomain,
								new_domain: newDomain
							}, function(response) {
								if (response.success) {
									alert(self.config.strings.successRedirect);
									window.location.href = response.data.redirect_url;
								} else {
									$btn.prop("disabled", false).html(\'<span class="dashicons dashicons-update"></span>\' + self.config.strings.changeDomainNow);
									alert(response.data || self.config.strings.errorChanging);
								}
							}).fail(function() {
								$btn.prop("disabled", false).html(\'<span class="dashicons dashicons-update"></span>\' + self.config.strings.changeDomainNow);
								alert(self.config.strings.errorChanging);
							});
						});
					},
					updateChangeButton: function() {
						var oldDomain = $("#ap-old-domain").val().trim();
						var newDomain = $("#ap-new-domain").val().trim();
						$("#ap-domain-change-btn").prop("disabled", !(oldDomain && newDomain && oldDomain !== newDomain && this.previewDone));
					}
				};
				DomainChanger.init();
			});'
		);
	}

	/**
	 * Process settings
	 *
	 * @param array $data Form data.
	 */
	protected function process_settings( $data ) {
		$settings = array(
			'backup_before_change' => isset( $data['backup_before_change'] ),
			'last_change_date'     => $this->get_setting( 'last_change_date', '' ),
			'last_old_domain'      => $this->get_setting( 'last_old_domain', '' ),
			'last_new_domain'      => $this->get_setting( 'last_new_domain', '' ),
		);

		$this->save_settings( $settings );

		add_action( 'admin_notices', function () {
			blaminhor_essentials_admin_notice( __( 'Settings saved.', 'blaminhor-essentials' ), 'success' );
		} );
	}

	/**
	 * Check if backup module is active
	 *
	 * @return bool
	 */
	private function is_backup_module_active() {
		$active_modules = get_option( 'blaminhor_essentials_active_modules', array() );
		return in_array( 'backup', $active_modules, true );
	}

	/**
	 * Ensure backup directory exists and is protected
	 */
	private function ensure_backup_directory() {
		if ( ! file_exists( $this->backup_dir ) ) {
			wp_mkdir_p( $this->backup_dir );
		}

		// Create .htaccess to protect backups.
		$htaccess = $this->backup_dir . '/.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			file_put_contents( $htaccess, "Order deny,allow\nDeny from all" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		}

		// Create index.php for additional protection.
		$index = $this->backup_dir . '/index.php';
		if ( ! file_exists( $index ) ) {
			file_put_contents( $index, '<?php // Silence is golden.' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		}
	}

	/**
	 * Create database backup
	 *
	 * @return array|WP_Error
	 */
	private function create_database_backup() {
		global $wpdb;

		if ( ! class_exists( 'ZipArchive' ) ) {
			return new WP_Error( 'no_zip', __( 'ZipArchive extension is not available.', 'blaminhor-essentials' ) );
		}

		$this->ensure_backup_directory();

		$site_name = sanitize_title( get_bloginfo( 'name' ) );
		$site_name = substr( $site_name, 0, 30 ) ?: 'wordpress';
		$timestamp = gmdate( 'Y-m-d-His' );
		$sql_temp  = $this->backup_dir . '/temp-domain-change-' . $timestamp . '.sql';
		$filename  = $site_name . '-' . $timestamp . '-domain-change.zip';
		$filepath  = $this->backup_dir . '/' . $filename;

		$tables = $wpdb->get_results( 'SHOW TABLES', ARRAY_N ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery

		if ( empty( $tables ) ) {
			return new WP_Error( 'no_tables', __( 'No database tables found.', 'blaminhor-essentials' ) );
		}

		// Start with SET NAMES to ensure proper encoding.
		$sql  = "SET NAMES utf8mb4;\n";
		$sql .= "SET CHARACTER SET utf8mb4;\n\n";
		$sql .= "-- Blaminhor Essentials Database Backup (Before Domain Change)\n";
		$sql .= '-- Site: ' . html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES, 'UTF-8' ) . "\n";
		$sql .= '-- URL: ' . home_url() . "\n";
		$sql .= '-- Generated: ' . gmdate( 'Y-m-d H:i:s' ) . "\n\n";
		$sql .= "SET FOREIGN_KEY_CHECKS = 0;\n\n";

		foreach ( $tables as $table ) {
			$table_name = $table[0];

			// Only backup tables with our prefix.
			if ( strpos( $table_name, $wpdb->prefix ) !== 0 ) {
				continue;
			}

			// Validate table name contains only safe characters.
			if ( preg_match( '/[^a-zA-Z0-9_]/', $table_name ) ) {
				continue;
			}

			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
			$create = $wpdb->get_row(
				$wpdb->prepare( 'SHOW CREATE TABLE %i', $table_name ),
				ARRAY_N
			);
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange
			if ( $create ) {
				$sql .= $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table_name ) . ";\n";
				$sql .= $create[1] . ";\n\n";
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Required for domain change export
			$rows = $wpdb->get_results(
				$wpdb->prepare( 'SELECT * FROM %i', $table_name ),
				ARRAY_A
			);

			if ( ! empty( $rows ) ) {
				$columns         = array_keys( $rows[0] );
				$columns_escaped = array_map(
					function ( $col ) {
						return '`' . esc_sql( $col ) . '`';
					},
					$columns
				);

				// Prepare table identifier once for this table.
				$table_identifier = $wpdb->prepare( '%i', $table_name );

				foreach ( array_chunk( $rows, 100 ) as $chunk ) {
					$sql .= 'INSERT INTO ' . $table_identifier . ' (' . implode( ', ', $columns_escaped ) . ") VALUES\n";

					$values = array();
					foreach ( $chunk as $row ) {
						$row_values = array_map(
							function ( $value ) use ( $wpdb ) {
								if ( is_null( $value ) ) {
									return 'NULL';
								}
								return "'" . $wpdb->_real_escape( $value ) . "'";
							},
							array_values( $row )
						);
						$values[] = '(' . implode( ', ', $row_values ) . ')';
					}

					$sql .= implode( ",\n", $values ) . ";\n\n";
				}
			}
		}

		$sql .= "SET FOREIGN_KEY_CHECKS = 1;\n";

		// Write SQL to temp file.
		$written = file_put_contents( $sql_temp, $sql ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		if ( false === $written ) {
			return new WP_Error( 'write_failed', __( 'Failed to write backup file.', 'blaminhor-essentials' ) );
		}

		// Wrap in a ZIP archive (compatible with the Backup module format).
		$zip = new ZipArchive();
		if ( $zip->open( $filepath, ZipArchive::CREATE | ZipArchive::OVERWRITE ) !== true ) {
			wp_delete_file( $sql_temp );
			return new WP_Error( 'zip_failed', __( 'Failed to create backup archive.', 'blaminhor-essentials' ) );
		}
		$zip->addFile( $sql_temp, 'database.sql' );
		$zip->close();
		wp_delete_file( $sql_temp );

		return array(
			'filename' => $filename,
			'path'     => $filepath,
			'size'     => filesize( $filepath ),
		);
	}

	/**
	 * AJAX: Create database backup
	 */
	public function ajax_create_backup() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'Unauthorized', 'blaminhor-essentials' ) );
		}

		$result = $this->create_database_backup();

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( $result->get_error_message() );
		}

		wp_send_json_success(
			array(
				'message'  => __( 'Backup created successfully.', 'blaminhor-essentials' ),
				'filename' => $result['filename'],
				'size'     => size_format( $result['size'] ),
			)
		);
	}

	/**
	 * Get current domain
	 *
	 * @return string
	 */
	private function get_current_domain() {
		$home_url = home_url();
		$parsed   = wp_parse_url( $home_url );
		return isset( $parsed['host'] ) ? $parsed['host'] : '';
	}

	/**
	 * AJAX: Preview changes
	 */
	public function ajax_preview_changes() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'Unauthorized', 'blaminhor-essentials' ) );
		}

		$old_domain = isset( $_POST['old_domain'] ) ? sanitize_text_field( wp_unslash( $_POST['old_domain'] ) ) : '';
		$new_domain = isset( $_POST['new_domain'] ) ? sanitize_text_field( wp_unslash( $_POST['new_domain'] ) ) : '';

		if ( empty( $old_domain ) ) {
			wp_send_json_error( __( 'Please enter a search domain.', 'blaminhor-essentials' ) );
		}

		if ( empty( $new_domain ) ) {
			wp_send_json_error( __( 'Please enter a new domain.', 'blaminhor-essentials' ) );
		}

		// Clean up domains.
		$old_domain = preg_replace( '#^https?://#', '', $old_domain );
		$old_domain = rtrim( $old_domain, '/' );
		$new_domain = preg_replace( '#^https?://#', '', $new_domain );
		$new_domain = rtrim( $new_domain, '/' );

		if ( $new_domain === $old_domain ) {
			wp_send_json_error( __( 'The search domain and new domain must be different.', 'blaminhor-essentials' ) );
		}

		global $wpdb;

		$preview = array();
		$tables  = $this->get_all_searchable_tables();

		foreach ( $tables as $table_info ) {
			$table_name = $table_info['table'];
			$columns    = $table_info['columns'];

			// Build WHERE clause for all text columns.
			$where_parts = array();
			foreach ( $columns as $column ) {
				$where_parts[] = $wpdb->prepare(
					'`' . esc_sql( $column ) . '` LIKE %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
					'%' . $wpdb->esc_like( $old_domain ) . '%'
				);
			}

			if ( empty( $where_parts ) ) {
				continue;
			}

			$where = implode( ' OR ', $where_parts );

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic table/column scan
			$count = $wpdb->get_var( "SELECT COUNT(*) FROM `" . esc_sql( $table_name ) . "` WHERE {$where}" );

			if ( $count > 0 ) {
				// Determine display name.
				$display_name = str_replace( $wpdb->prefix, '', $table_name );

				$preview[] = array(
					'table' => $table_name,
					'count' => (int) $count,
					/* translators: 1: number of rows, 2: table name */
					'label' => sprintf( __( '%1$d rows in %2$s', 'blaminhor-essentials' ), $count, $display_name ),
				);
			}
		}

		$total = array_sum( array_column( $preview, 'count' ) );

		wp_send_json_success(
			array(
				'old_domain' => $old_domain,
				'new_domain' => $new_domain,
				'preview'    => $preview,
				'total'      => $total,
			)
		);
	}

	/**
	 * Get all searchable tables with their text columns
	 *
	 * @return array Array of arrays with 'table' and 'columns' keys.
	 */
	private function get_all_searchable_tables() {
		global $wpdb;

		$tables = array();

		// Get all tables with our prefix.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$all_tables = $wpdb->get_col(
			$wpdb->prepare(
				'SHOW TABLES LIKE %s',
				$wpdb->esc_like( $wpdb->prefix ) . '%'
			)
		);

		if ( empty( $all_tables ) ) {
			return $tables;
		}

		$text_types = array( 'varchar', 'text', 'longtext', 'mediumtext', 'tinytext' );

		foreach ( $all_tables as $table_name ) {
			// Validate table name.
			if ( preg_match( '/[^a-zA-Z0-9_]/', $table_name ) ) {
				continue;
			}

			// Get text columns.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Dynamic table introspection
			$columns = $wpdb->get_results( "SHOW COLUMNS FROM `" . esc_sql( $table_name ) . "`" );

			$text_columns = array();
			foreach ( $columns as $column ) {
				$col_type = strtolower( $column->Type );
				foreach ( $text_types as $type ) {
					if ( strpos( $col_type, $type ) !== false ) {
						$text_columns[] = $column->Field;
						break;
					}
				}
			}

			if ( ! empty( $text_columns ) ) {
				$tables[] = array(
					'table'   => $table_name,
					'columns' => $text_columns,
				);
			}
		}

		return $tables;
	}

	/**
	 * Recursively replace serialized data
	 *
	 * @param mixed  $data       Data to process.
	 * @param string $old_domain Old domain.
	 * @param string $new_domain New domain.
	 * @return mixed
	 */
	private function replace_in_serialized( $data, $old_domain, $new_domain ) {
		if ( is_string( $data ) ) {
			// Check if it's serialized data.
			$unserialized = @unserialize( $data ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize,WordPress.PHP.NoSilencedErrors.Discouraged
			if ( false !== $unserialized && is_array( $unserialized ) ) {
				// Process the unserialized array.
				$processed = $this->replace_in_serialized( $unserialized, $old_domain, $new_domain );
				return serialize( $processed ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
			}

			// Check if it's JSON.
			$json = json_decode( $data, true );
			if ( json_last_error() === JSON_ERROR_NONE && is_array( $json ) ) {
				$processed = $this->replace_in_serialized( $json, $old_domain, $new_domain );
				return wp_json_encode( $processed );
			}

			// Simple string replacement (with and without protocol).
			$data = str_replace( 'https://' . $old_domain, 'https://' . $new_domain, $data );
			$data = str_replace( 'http://' . $old_domain, 'http://' . $new_domain, $data );
			$data = str_replace( '//' . $old_domain, '//' . $new_domain, $data );

			return $data;
		}

		if ( is_array( $data ) ) {
			foreach ( $data as $key => $value ) {
				$data[ $key ] = $this->replace_in_serialized( $value, $old_domain, $new_domain );
			}
			return $data;
		}

		return $data;
	}

	/**
	 * AJAX: Change domain
	 */
	public function ajax_change_domain() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( __( 'Unauthorized', 'blaminhor-essentials' ) );
		}

		$old_domain = isset( $_POST['old_domain'] ) ? sanitize_text_field( wp_unslash( $_POST['old_domain'] ) ) : '';
		$new_domain = isset( $_POST['new_domain'] ) ? sanitize_text_field( wp_unslash( $_POST['new_domain'] ) ) : '';

		if ( empty( $old_domain ) ) {
			wp_send_json_error( __( 'Please enter a search domain.', 'blaminhor-essentials' ) );
		}

		if ( empty( $new_domain ) ) {
			wp_send_json_error( __( 'Please enter a new domain.', 'blaminhor-essentials' ) );
		}

		// Clean up domains.
		$old_domain = preg_replace( '#^https?://#', '', $old_domain );
		$old_domain = rtrim( $old_domain, '/' );
		$new_domain = preg_replace( '#^https?://#', '', $new_domain );
		$new_domain = rtrim( $new_domain, '/' );

		if ( $new_domain === $old_domain ) {
			wp_send_json_error( __( 'The search domain and new domain must be different.', 'blaminhor-essentials' ) );
		}

		global $wpdb;

		$changes_made   = 0;
		$current_domain = $this->get_current_domain();

		// Update siteurl and home only if old_domain matches the current domain.
		if ( $old_domain === $current_domain ) {
			$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
				$wpdb->prepare(
					"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = 'siteurl'",
					'https://' . $new_domain
				)
			);
			$changes_made++;

			$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
				$wpdb->prepare(
					"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = 'home'",
					'https://' . $new_domain
				)
			);
			$changes_made++;
		}

		// Process all tables with serialized-safe replacement.
		$tables = $this->get_all_searchable_tables();

		foreach ( $tables as $table_info ) {
			$table_name = $table_info['table'];
			$columns    = $table_info['columns'];

			// Build WHERE clause.
			$where_parts = array();
			foreach ( $columns as $column ) {
				$where_parts[] = $wpdb->prepare(
					'%i LIKE %s',
					$column,
					'%' . $wpdb->esc_like( $old_domain ) . '%'
				);
			}

			if ( empty( $where_parts ) ) {
				continue;
			}

			$where = implode( ' OR ', $where_parts );

			// Get primary key for this table.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$pk_row = $wpdb->get_row( $wpdb->prepare( 'SHOW KEYS FROM %i WHERE Key_name = %s', $table_name, 'PRIMARY' ) );
			$pk     = $pk_row ? $pk_row->Column_name : null;

			if ( ! $pk ) {
				// No primary key: use simple SQL REPLACE for each column.
				foreach ( $columns as $column ) {
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
					$wpdb->query(
						$wpdb->prepare(
							'UPDATE %i SET %i = REPLACE(%i, %s, %s) WHERE %i LIKE %s',
							$table_name,
							$column,
							$column,
							$old_domain,
							$new_domain,
							$column,
							'%' . $wpdb->esc_like( $old_domain ) . '%'
						)
					);
					$changes_made += (int) $wpdb->rows_affected;
				}
				continue;
			}

			// Build SELECT column list with %i identifiers (pre-prepared to avoid double-prepare on $where).
			$select_ids      = array_merge( array( $pk ), $columns );
			$select_cols     = $wpdb->prepare( implode( ', ', array_fill( 0, count( $select_ids ), '%i' ) ), ...$select_ids ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$table_prepared  = $wpdb->prepare( '%i', $table_name );

			// Skip siteurl/home in options table (already handled above).
			$extra_where = '';
			if ( $table_name === $wpdb->options && $old_domain === $current_domain ) {
				$extra_where = $wpdb->prepare( ' AND option_name NOT IN (%s, %s)', 'siteurl', 'home' );
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- All interpolated parts ($select_cols, $table_prepared, $where, $extra_where) are pre-prepared via $wpdb->prepare()
			$rows = $wpdb->get_results( "SELECT {$select_cols} FROM {$table_prepared} WHERE ({$where}){$extra_where}" );

			foreach ( $rows as $row ) {
				$pk_value = $row->$pk;
				$updates  = array();

				foreach ( $columns as $column ) {
					if ( ! isset( $row->$column ) || strpos( $row->$column, $old_domain ) === false ) {
						continue;
					}

					$new_value = $this->replace_in_serialized( $row->$column, $old_domain, $new_domain );
					if ( $new_value !== $row->$column ) {
						$updates[ $column ] = $new_value;
					}
				}

				if ( ! empty( $updates ) ) {
					// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
					$wpdb->update(
						$table_name,
						$updates,
						array( $pk => $pk_value )
					);
					$changes_made++;
				}
			}
		}

		// Save the change history.
		$settings                     = $this->settings;
		$settings['last_change_date'] = gmdate( 'Y-m-d H:i:s' );
		$settings['last_old_domain']  = $old_domain;
		$settings['last_new_domain']  = $new_domain;
		$this->save_settings( $settings );

		// Clear all caches.
		wp_cache_flush();

		// Notify other Blaminhor Essentials modules (page cache, CDN, edge) that
		// every cached HTML now references the old domain and must be regenerated.
		// The Cache module listens for this action via class-cache-purge.php.
		do_action( 'blaminhor_essentials_cache_purge_request' );

		// Redirect to new domain only if the current domain was changed.
		$redirect_domain = ( $old_domain === $current_domain ) ? $new_domain : $current_domain;

		wp_send_json_success(
			array(
				'message'      => __( 'Domain changed successfully!', 'blaminhor-essentials' ),
				'old_domain'   => $old_domain,
				'new_domain'   => $new_domain,
				'changes_made' => $changes_made,
				'redirect_url' => 'https://' . $redirect_domain . '/wp-admin/admin.php?page=blaminhor-essentials-domain-changer&domain-changed=1',
			)
		);
	}

	/**
	 * Render settings page
	 */
	public function render_settings_page() {
		$current_domain   = $this->get_current_domain();
		$backup_active    = $this->is_backup_module_active();
		$last_change_date = $this->get_setting( 'last_change_date', '' );
		$last_old_domain  = $this->get_setting( 'last_old_domain', '' );
		$last_new_domain  = $this->get_setting( 'last_new_domain', '' );
		?>
		<div class="wrap blaminhor-essentials-wrap">
			<div class="blaminhor-essentials-header">
				<h1><?php esc_html_e( 'Domain Changer', 'blaminhor-essentials' ); ?></h1>
				<p class="blaminhor-essentials-tagline">
					<?php esc_html_e( 'Change your site domain and update all database references in one click.', 'blaminhor-essentials' ); ?>
				</p>
			</div>

			<?php // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just checking flag for success message display ?>
			<?php if ( isset( $_GET['domain-changed'] ) ) : ?>
				<div class="blaminhor-essentials-notice success ap-mb-lg">
					<p><strong><?php esc_html_e( 'Domain changed successfully!', 'blaminhor-essentials' ); ?></strong></p>
					<p><?php esc_html_e( 'All database references have been updated. Please update your DNS settings if needed.', 'blaminhor-essentials' ); ?></p>
				</div>
			<?php endif; ?>

			<div class="blaminhor-essentials-section">
				<div class="blaminhor-essentials-tabs-wrapper">
					<div class="blaminhor-essentials-tabs">
						<button type="button" class="blaminhor-essentials-tab active" data-tab="change"><?php esc_html_e( 'Change Domain', 'blaminhor-essentials' ); ?></button>
						<button type="button" class="blaminhor-essentials-tab" data-tab="settings"><?php esc_html_e( 'Settings', 'blaminhor-essentials' ); ?></button>
					</div>

					<!-- Change Domain Tab -->
					<div class="blaminhor-essentials-tab-content active" data-tab-content="change">
						<div class="ap-db-summary-cards ap-mb-lg">
							<div class="ap-db-card ap-db-card-info">
								<div class="ap-db-card-icon">
									<span class="dashicons dashicons-admin-site-alt3"></span>
								</div>
								<div class="ap-db-card-value"><?php echo esc_html( $current_domain ); ?></div>
								<div class="ap-db-card-label"><?php esc_html_e( 'Current Domain', 'blaminhor-essentials' ); ?></div>
							</div>
						</div>

						<?php if ( ! empty( $last_change_date ) ) : ?>
							<div class="blaminhor-essentials-notice info ap-mb-lg">
								<p>
									<?php
									printf(
										/* translators: 1: old domain, 2: new domain, 3: date */
										esc_html__( 'Last change: %1$s to %2$s on %3$s', 'blaminhor-essentials' ),
										'<strong>' . esc_html( $last_old_domain ) . '</strong>',
										'<strong>' . esc_html( $last_new_domain ) . '</strong>',
										esc_html( $last_change_date )
									);
									?>
								</p>
							</div>
						<?php endif; ?>

						<!-- Step 1: Backup -->
						<div class="ap-domain-step">
							<h3>
								<span class="ap-step-number">1</span>
								<?php esc_html_e( 'Create a Backup', 'blaminhor-essentials' ); ?>
							</h3>
							<p class="description"><?php esc_html_e( 'We strongly recommend creating a database backup before changing your domain.', 'blaminhor-essentials' ); ?></p>

							<div id="ap-domain-backup-status" class="ap-mt-md">
								<?php if ( $backup_active ) : ?>
									<button type="button" id="ap-domain-backup-btn" class="button button-secondary ap-btn-icon">
										<span class="dashicons dashicons-cloud-saved"></span>
										<?php esc_html_e( 'Create Database Backup', 'blaminhor-essentials' ); ?>
									</button>
								<?php else : ?>
									<p class="description ap-desc-danger">
										<?php esc_html_e( 'The Backup module is not active.', 'blaminhor-essentials' ); ?>
										<a href="<?php echo esc_url( admin_url( 'admin.php?page=blaminhor-essentials' ) ); ?>"><?php esc_html_e( 'Activate it', 'blaminhor-essentials' ); ?></a>
										<?php esc_html_e( 'or proceed without backup (not recommended).', 'blaminhor-essentials' ); ?>
									</p>
									<button type="button" id="ap-domain-backup-btn" class="button button-secondary ap-btn-icon">
										<span class="dashicons dashicons-cloud-saved"></span>
										<?php esc_html_e( 'Create Database Backup Anyway', 'blaminhor-essentials' ); ?>
									</button>
								<?php endif; ?>
							</div>
						</div>

						<!-- Step 2: Search Domain -->
						<div class="ap-domain-step">
							<h3>
								<span class="ap-step-number">2</span>
								<?php esc_html_e( 'Search Domain', 'blaminhor-essentials' ); ?>
							</h3>

							<table class="form-table" style="margin: 0;">
								<tr>
									<th scope="row">
										<label for="ap-old-domain"><?php esc_html_e( 'Search Domain', 'blaminhor-essentials' ); ?></label>
									</th>
									<td>
										<input type="text" id="ap-old-domain" class="regular-text" value="<?php echo esc_attr( $current_domain ); ?>" autocomplete="off">
										<p class="description"><?php esc_html_e( 'The domain to search for in the database. Pre-filled with the current domain.', 'blaminhor-essentials' ); ?></p>
									</td>
								</tr>
							</table>

							<div id="ap-domain-diff-notice" class="blaminhor-essentials-notice warning ap-hidden ap-mt-md ap-mb-md">
								<p><?php esc_html_e( 'The search domain differs from the current site domain. This mode allows you to find and replace traces of a previously migrated domain.', 'blaminhor-essentials' ); ?></p>
							</div>
						</div>

						<!-- Step 3: Replace With -->
						<div class="ap-domain-step">
							<h3>
								<span class="ap-step-number">3</span>
								<?php esc_html_e( 'Replace With', 'blaminhor-essentials' ); ?>
							</h3>

							<table class="form-table" style="margin: 0;">
								<tr>
									<th scope="row">
										<label for="ap-new-domain"><?php esc_html_e( 'New Domain', 'blaminhor-essentials' ); ?></label>
									</th>
									<td>
										<input type="text" id="ap-new-domain" class="regular-text" placeholder="example.com" autocomplete="off">
										<p class="description"><?php esc_html_e( 'Enter the new domain without http:// or https://. Example: newsite.com', 'blaminhor-essentials' ); ?></p>
									</td>
								</tr>
							</table>

							<div class="ap-mt-md">
								<button type="button" id="ap-domain-preview-btn" class="button button-secondary">
									<?php esc_html_e( 'Preview Changes', 'blaminhor-essentials' ); ?>
								</button>
							</div>
						</div>

						<!-- Preview Area -->
						<div id="ap-domain-preview" class="ap-domain-preview ap-hidden">
							<h3><?php esc_html_e( 'Preview of Changes', 'blaminhor-essentials' ); ?></h3>
							<div id="ap-domain-preview-content"></div>
						</div>

						<!-- Step 4: Execute -->
						<div class="ap-domain-step">
							<h3>
								<span class="ap-step-number">4</span>
								<?php esc_html_e( 'Execute Domain Change', 'blaminhor-essentials' ); ?>
							</h3>
							<p class="description"><?php esc_html_e( 'This will replace all occurrences of the old domain with the new domain in your database.', 'blaminhor-essentials' ); ?></p>

							<div class="blaminhor-essentials-notice warning" style="margin: 15px 0;">
								<p><strong><?php esc_html_e( 'Warning:', 'blaminhor-essentials' ); ?></strong> <?php esc_html_e( 'This action cannot be undone. Make sure you have a backup before proceeding.', 'blaminhor-essentials' ); ?></p>
							</div>

							<button type="button" id="ap-domain-change-btn" class="button button-primary button-hero ap-btn-icon" disabled>
								<span class="dashicons dashicons-update"></span>
								<?php esc_html_e( 'Change Domain Now', 'blaminhor-essentials' ); ?>
							</button>
						</div>
					</div>

					<!-- Settings Tab -->
					<div class="blaminhor-essentials-tab-content" data-tab-content="settings">
						<form method="post">
							<?php $this->render_nonce_field(); ?>

							<table class="form-table">
								<tr>
									<th scope="row"><?php esc_html_e( 'Backup', 'blaminhor-essentials' ); ?></th>
									<td>
										<label>
											<input type="checkbox" name="backup_before_change" <?php checked( $this->get_setting( 'backup_before_change', true ) ); ?>>
											<?php esc_html_e( 'Remind to create backup before domain change', 'blaminhor-essentials' ); ?>
										</label>
									</td>
								</tr>
							</table>

							<p class="submit">
								<button type="submit" class="button button-primary">
									<?php esc_html_e( 'Save Settings', 'blaminhor-essentials' ); ?>
								</button>
							</p>
						</form>
					</div>
				</div>
			</div>
		</div>
		<?php
	}
}
