<?php
/**
 * Redirections Module
 *
 * Manage 301 redirections with simple rules or regex patterns.
 *
 * @package BlaminhorEssentials
 */

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

/**
 * Redirections module class
 */
class Blaminhor_Essentials_Module_Redirections extends Blaminhor_Essentials_Module {

	/**
	 * Module ID
	 *
	 * @var string
	 */
	protected $id = 'redirections';

	/**
	 * Module name
	 *
	 * @var string
	 */
	protected $name = 'Redirections 301';

	/**
	 * Table name for redirections
	 *
	 * @var string
	 */
	private $table_name;

	/**
	 * Initialize module
	 */
	protected function init() {
		global $wpdb;
		$this->name        = __( 'Redirections 301', 'blaminhor-essentials' );
		$this->description = __( 'Manage 301 redirections with simple rules or regex patterns.', 'blaminhor-essentials' );
		$this->table_name  = $wpdb->prefix . 'blaminhor_essentials_redirections';
	}

	/**
	 * Get default settings
	 *
	 * @return array
	 */
	protected function get_default_settings() {
		return array(
			'auto_redirect_on_slug_change' => true,
			'log_redirects'                => false,
			'case_insensitive'             => true,
		);
	}

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

		// Frontend redirect.
		add_action( 'template_redirect', array( $this, 'maybe_redirect' ), 1 );

		// Slug change detection.
		if ( $this->get_setting( 'auto_redirect_on_slug_change', true ) ) {
			add_action( 'post_updated', array( $this, 'detect_slug_change' ), 10, 3 );
			add_action( 'edited_term', array( $this, 'detect_term_slug_change' ), 10, 3 );
			// Store old term slug before update (when edit page loads).
			add_action( 'load-edit-tags.php', array( $this, 'maybe_store_old_term_slug' ) );
			add_action( 'load-term.php', array( $this, 'maybe_store_old_term_slug' ) );
		}

		// Admin notices for slug changes.
		add_action( 'admin_notices', array( $this, 'show_slug_change_notice' ) );
		add_action( 'admin_init', array( $this, 'handle_slug_redirect_action' ) );

		// AJAX handlers.
		add_action( 'wp_ajax_ap_redirections_add', array( $this, 'ajax_add_redirection' ) );
		add_action( 'wp_ajax_ap_redirections_delete', array( $this, 'ajax_delete_redirection' ) );
		add_action( 'wp_ajax_ap_redirections_toggle', array( $this, 'ajax_toggle_redirection' ) );
		add_action( 'wp_ajax_ap_redirections_import', array( $this, 'ajax_import_csv' ) );
		add_action( 'wp_ajax_ap_redirections_get', array( $this, 'ajax_get_redirections' ) );
		add_action( 'wp_ajax_ap_redirections_edit', array( $this, 'ajax_edit_redirection' ) );
		add_action( 'wp_ajax_ap_redirections_import_plugin', array( $this, 'ajax_import_from_plugin' ) );
		add_action( 'wp_ajax_ap_redirections_bulk_delete', array( $this, 'ajax_bulk_delete' ) );
	}

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

		$config = array(
			'nonce'   => wp_create_nonce( 'blaminhor_essentials_admin' ),
			'ajaxurl' => admin_url( 'admin-ajax.php' ),
			'strings' => array(
				'loading'              => __( 'Loading...', 'blaminhor-essentials' ),
				'noRedirections'       => __( 'No redirections found.', 'blaminhor-essentials' ),
				'active'               => __( 'Active', 'blaminhor-essentials' ),
				'inactive'             => __( 'Inactive', 'blaminhor-essentials' ),
				'simple'               => __( 'Simple', 'blaminhor-essentials' ),
				'toggle'               => __( 'Toggle', 'blaminhor-essentials' ),
				'edit'                 => __( 'Edit', 'blaminhor-essentials' ),
				'deleteTitle'          => __( 'Delete', 'blaminhor-essentials' ),
				'confirmDelete'        => __( 'Are you sure you want to delete this redirection?', 'blaminhor-essentials' ),
				'adding'               => __( 'Adding...', 'blaminhor-essentials' ),
				'error'                => __( 'An error occurred.', 'blaminhor-essentials' ),
				'saving'               => __( 'Saving...', 'blaminhor-essentials' ),
				'saveChanges'          => __( 'Save Changes', 'blaminhor-essentials' ),
				'selectFile'           => __( 'Please select a CSV file to import.', 'blaminhor-essentials' ),
				/* translators: %d: number of redirections imported */
				'importSuccess'        => __( 'Successfully imported %d redirections.', 'blaminhor-essentials' ),
				'someErrors'           => __( 'Some errors occurred:', 'blaminhor-essentials' ),
				'importFailed'         => __( 'Import failed.', 'blaminhor-essentials' ),
				'redirectionsImported' => __( 'redirections imported', 'blaminhor-essentials' ),
				'duplicateExists'      => __( 'A redirection with this source URL already exists.', 'blaminhor-essentials' ),
				/* translators: %d: number of selected items */
				'selectedCount'        => __( '%d selected', 'blaminhor-essentials' ),
				'confirmBulkDelete'    => __( 'Are you sure you want to delete the selected redirections?', 'blaminhor-essentials' ),
				'deleting'             => __( 'Deleting...', 'blaminhor-essentials' ),
				'loopWarning'          => __( 'Loop', 'blaminhor-essentials' ),
				'loopTooltip'          => __( 'This redirection creates a loop and may cause issues.', 'blaminhor-essentials' ),
				/* translators: %d: number of loop redirections */
				'loopBanner'           => __( '%d redirection(s) detected as loops have been automatically disabled.', 'blaminhor-essentials' ),
			),
		);

		wp_add_inline_script(
			'blaminhor-essentials-modules',
			'jQuery(document).ready(function($) { BlaminhorEssentialsRedirections.init(' . wp_json_encode( $config ) . '); });'
		);
	}

	/**
	 * Create redirections table
	 */
	public function maybe_create_table() {
		global $wpdb;

		$table_exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare( 'SHOW TABLES LIKE %s', $this->table_name )
		);

		if ( $table_exists ) {
			return;
		}

		$charset_collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE {$this->table_name} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			source_url varchar(2048) NOT NULL,
			target_url varchar(2048) NOT NULL,
			is_regex tinyint(1) NOT NULL DEFAULT 0,
			is_active tinyint(1) NOT NULL DEFAULT 1,
			hit_count bigint(20) unsigned NOT NULL DEFAULT 0,
			created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
			updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
			PRIMARY KEY (id),
			KEY source_url (source_url(191)),
			KEY is_active (is_active)
		) $charset_collate;";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
	}

	/**
	 * Process settings
	 *
	 * @param array $data Form data.
	 */
	protected function process_settings( $data ) {
		$settings = array(
			'auto_redirect_on_slug_change' => isset( $data['auto_redirect_on_slug_change'] ),
			'log_redirects'                => isset( $data['log_redirects'] ),
			'case_insensitive'             => isset( $data['case_insensitive'] ),
		);

		$this->save_settings( $settings );

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

	/**
	 * Check if URL matches a redirect rule
	 */
	public function maybe_redirect() {
		if ( is_admin() ) {
			return;
		}

		global $wpdb;

		$request_uri    = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$request_path   = wp_parse_url( $request_uri, PHP_URL_PATH );
		$case_sensitive = ! $this->get_setting( 'case_insensitive', true );

		// Get all active redirections.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Caching not needed for redirect lookup
		$redirections = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT * FROM %i WHERE is_active = 1 ORDER BY is_regex ASC, id ASC',
				$this->table_name
			)
		);

		if ( empty( $redirections ) ) {
			return;
		}

		foreach ( $redirections as $redirect ) {
			$matched = false;

			if ( $redirect->is_regex ) {
				// Regex match.
				$pattern = $redirect->source_url;
				if ( ! $case_sensitive ) {
					$pattern .= 'i';
				}
				if ( @preg_match( $pattern, $request_path ) ) {
					$matched    = true;
					$target_url = preg_replace( $pattern, $redirect->target_url, $request_path );
				}
			} else {
				// Simple match.
				$source = $redirect->source_url;
				$match  = $request_path;

				if ( ! $case_sensitive ) {
					$source = strtolower( $source );
					$match  = strtolower( $match );
				}

				// Also check without trailing slash.
				if ( $source === $match || rtrim( $source, '/' ) === rtrim( $match, '/' ) ) {
					$matched    = true;
					$target_url = $redirect->target_url;
				}
			}

			if ( $matched && ! empty( $target_url ) ) {
				// Increment hit count.
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update needed for hit tracking
				$wpdb->query(
					$wpdb->prepare(
						'UPDATE %i SET hit_count = hit_count + 1 WHERE id = %d',
						$this->table_name,
						$redirect->id
					)
				);

				// Ensure target URL is absolute.
				if ( strpos( $target_url, 'http' ) !== 0 ) {
					$target_url = home_url( $target_url );
				}

				// Perform redirect.
				wp_safe_redirect( $target_url, 301 );
				exit;
			}
		}
	}

	/**
	 * Detect slug change on post update
	 *
	 * @param int     $post_id     Post ID.
	 * @param WP_Post $post_after  Post after update.
	 * @param WP_Post $post_before Post before update.
	 */
	public function detect_slug_change( $post_id, $post_after, $post_before ) {
		// Only for public post types.
		$post_type_obj = get_post_type_object( $post_after->post_type );
		if ( ! $post_type_obj || ! $post_type_obj->public ) {
			return;
		}

		// Check if slug changed.
		if ( $post_before->post_name === $post_after->post_name ) {
			return;
		}

		// Skip if old slug is empty (new post, not a real slug change).
		if ( empty( $post_before->post_name ) ) {
			return;
		}

		// Skip auto-drafts, revisions, and trashed posts.
		if ( in_array( $post_after->post_status, array( 'auto-draft', 'inherit', 'trash' ), true ) ) {
			return;
		}

		// Get the new URL.
		$new_url = str_replace( home_url(), '', get_permalink( $post_id ) );

		// Recalculate old URL by replacing the new slug with the old one.
		$old_url = str_replace( $post_after->post_name, $post_before->post_name, $new_url );

		// Skip if URLs are the same (e.g., when using plain permalinks like ?page_id=X).
		if ( $old_url === $new_url ) {
			return;
		}

		// Store the change for notification.
		$pending = get_option( 'blaminhor_essentials_pending_redirects', array() );
		$pending[ $post_id ] = array(
			'type'     => 'post',
			'old_slug' => $post_before->post_name,
			'new_slug' => $post_after->post_name,
			'old_url'  => $old_url,
			'new_url'  => $new_url,
			'title'    => $post_after->post_title,
		);

		update_option( 'blaminhor_essentials_pending_redirects', $pending );
	}

	/**
	 * Detect term slug change
	 *
	 * @param int    $term_id  Term ID.
	 * @param int    $tt_id    Term taxonomy ID.
	 * @param string $taxonomy Taxonomy slug.
	 */
	public function detect_term_slug_change( $term_id, $tt_id, $taxonomy ) {
		$term = get_term( $term_id, $taxonomy );
		if ( is_wp_error( $term ) || ! $term ) {
			return;
		}

		// Get the old slug from cache/transient.
		$old_slug = get_transient( 'blaminhor_essentials_term_old_slug_' . $term_id );
		if ( ! $old_slug || $old_slug === $term->slug ) {
			return;
		}

		delete_transient( 'blaminhor_essentials_term_old_slug_' . $term_id );

		// Store the change for notification.
		$pending = get_option( 'blaminhor_essentials_pending_redirects', array() );
		$new_url = get_term_link( $term );
		if ( is_wp_error( $new_url ) ) {
			return;
		}

		$new_url = str_replace( home_url(), '', $new_url );
		$old_url = str_replace( $term->slug, $old_slug, $new_url );

		$pending[ 'term_' . $term_id ] = array(
			'type'     => 'term',
			'old_slug' => $old_slug,
			'new_slug' => $term->slug,
			'old_url'  => $old_url,
			'new_url'  => $new_url,
			'title'    => $term->name,
		);

		update_option( 'blaminhor_essentials_pending_redirects', $pending );
	}

	/**
	 * Maybe store old term slug when editing term page loads
	 */
	public function maybe_store_old_term_slug() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading term data on admin page load, no action taken
		if ( isset( $_GET['tag_ID'], $_GET['taxonomy'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Same as above
			$term_id  = absint( $_GET['tag_ID'] );
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$taxonomy = sanitize_key( $_GET['taxonomy'] );
			$term     = get_term( $term_id, $taxonomy );
			if ( ! is_wp_error( $term ) && $term ) {
				set_transient( 'blaminhor_essentials_term_old_slug_' . $term_id, $term->slug, HOUR_IN_SECONDS );
			}
		}
	}

	/**
	 * Show slug change notice
	 */
	public function show_slug_change_notice() {
		$pending = get_option( 'blaminhor_essentials_pending_redirects', array() );
		if ( empty( $pending ) ) {
			return;
		}

		// Use current page URL as base for action links (removes our params from current URL).
		$current_url = remove_query_arg( array( 'ap_redirect_action', 'ap_redirect_id', '_wpnonce' ) );

		foreach ( $pending as $id => $change ) {
			$add_url = wp_nonce_url(
				add_query_arg(
					array(
						'ap_redirect_action' => 'add',
						'ap_redirect_id'     => $id,
					),
					$current_url
				),
				'ap_redirect_action'
			);

			$dismiss_url = wp_nonce_url(
				add_query_arg(
					array(
						'ap_redirect_action' => 'dismiss',
						'ap_redirect_id'     => $id,
					),
					$current_url
				),
				'ap_redirect_action'
			);
			?>
			<div class="notice notice-info is-dismissible" style="padding: 12px;">
				<p style="margin: 0 0 8px; font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px;">
					<?php
					printf(
						/* translators: %s: Module name */
						esc_html__( 'Blaminhor Essentials – %s', 'blaminhor-essentials' ),
						esc_html__( 'Redirections 301', 'blaminhor-essentials' )
					);
					?>
				</p>
				<p style="margin: 0 0 10px;">
					<strong><?php esc_html_e( 'Slug changed:', 'blaminhor-essentials' ); ?></strong>
					<?php echo esc_html( $change['title'] ); ?>
				</p>
				<p style="margin: 0 0 10px; font-size: 12px; color: #666;">
					<?php echo esc_html( $change['old_url'] ); ?> &rarr; <?php echo esc_html( $change['new_url'] ); ?>
				</p>
				<p style="margin: 0;">
					<a href="<?php echo esc_url( $add_url ); ?>" class="button button-primary button-small">
						<?php esc_html_e( 'Add 301 Redirect', 'blaminhor-essentials' ); ?>
					</a>
					<a href="<?php echo esc_url( $dismiss_url ); ?>" class="button button-secondary button-small" style="margin-left: 5px;">
						<?php esc_html_e( 'Dismiss', 'blaminhor-essentials' ); ?>
					</a>
				</p>
			</div>
			<?php
		}
	}

	/**
	 * Handle slug redirect action
	 */
	public function handle_slug_redirect_action() {
		if ( ! isset( $_GET['ap_redirect_action'] ) || ! isset( $_GET['_wpnonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ap_redirect_action' ) ) {
			return;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$action = sanitize_key( $_GET['ap_redirect_action'] );
		$id     = isset( $_GET['ap_redirect_id'] ) ? sanitize_key( $_GET['ap_redirect_id'] ) : '';

		$pending = get_option( 'blaminhor_essentials_pending_redirects', array() );

		if ( ! isset( $pending[ $id ] ) ) {
			return;
		}

		if ( 'add' === $action ) {
			$this->add_redirection(
				$pending[ $id ]['old_url'],
				$pending[ $id ]['new_url'],
				false // Not regex.
			);
		}

		// Remove from pending.
		unset( $pending[ $id ] );
		update_option( 'blaminhor_essentials_pending_redirects', $pending );

		// Redirect back to referer or dashboard.
		$redirect_to = wp_get_referer();
		if ( ! $redirect_to ) {
			$redirect_to = admin_url();
		}
		// Remove our query args from referer.
		$redirect_to = remove_query_arg( array( 'ap_redirect_action', 'ap_redirect_id', '_wpnonce' ), $redirect_to );
		wp_safe_redirect( $redirect_to );
		exit;
	}

	/**
	 * Normalize a URL path for comparison
	 *
	 * @param string $url URL path.
	 * @return string Normalized path.
	 */
	private function normalize_path( $url ) {
		$path = wp_parse_url( $url, PHP_URL_PATH );
		if ( ! $path ) {
			$path = $url;
		}
		return strtolower( rtrim( $path, '/' ) );
	}

	/**
	 * Check if a redirection would create a loop
	 *
	 * Detects direct loops (source == target) and reverse loops (A→B exists, adding B→A).
	 *
	 * @param string   $source     Source URL.
	 * @param string   $target     Target URL.
	 * @param int|null $exclude_id Exclude this redirection ID from checks (for edits).
	 * @return bool True if loop detected.
	 */
	private function is_loop_redirection( $source, $target, $exclude_id = null ) {
		$norm_source = $this->normalize_path( $source );
		$norm_target = $this->normalize_path( $target );

		// Direct loop: source == target.
		if ( $norm_source === $norm_target ) {
			return true;
		}

		// Reverse loop: check if target→source already exists.
		global $wpdb;

		if ( $exclude_id ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$reverse = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COUNT(*) FROM %i WHERE is_regex = 0 AND id != %d',
					$this->table_name,
					$exclude_id
				)
			);
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$reverse = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COUNT(*) FROM %i WHERE is_regex = 0',
					$this->table_name
				)
			);
		}

		if ( ! $reverse ) {
			return false;
		}

		// Get all non-regex redirections to check for chain loops.
		if ( $exclude_id ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$all_redirections = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT id, source_url, target_url FROM %i WHERE is_regex = 0 AND id != %d',
					$this->table_name,
					$exclude_id
				)
			);
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$all_redirections = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT id, source_url, target_url FROM %i WHERE is_regex = 0',
					$this->table_name
				)
			);
		}

		// Build a map: normalized_source → normalized_target.
		$map = array();
		foreach ( $all_redirections as $r ) {
			$map[ $this->normalize_path( $r->source_url ) ] = $this->normalize_path( $r->target_url );
		}

		// Follow the chain from target to see if it leads back to source.
		$visited = array( $norm_source => true );
		$current = $norm_target;
		$max     = 20; // Prevent infinite loops.

		while ( isset( $map[ $current ] ) && $max-- > 0 ) {
			$next = $map[ $current ];
			if ( isset( $visited[ $next ] ) ) {
				return true; // Chain leads back to a visited node.
			}
			$visited[ $current ] = true;
			$current = $next;
		}

		return isset( $visited[ $current ] );
	}

	/**
	 * Get count of loop redirections
	 *
	 * @return int
	 */
	private function get_loop_redirections_count() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$redirections = $wpdb->get_results(
			$wpdb->prepare(
				'SELECT id, source_url, target_url, is_regex FROM %i',
				$this->table_name
			)
		);

		$count = 0;
		foreach ( $redirections as $r ) {
			if ( $r->is_regex ) {
				continue;
			}
			if ( $this->normalize_path( $r->source_url ) === $this->normalize_path( $r->target_url ) ) {
				++$count;
				continue;
			}
			// Check reverse: does another redirect point back?
			foreach ( $redirections as $other ) {
				if ( $other->id === $r->id || $other->is_regex ) {
					continue;
				}
				if ( $this->normalize_path( $other->source_url ) === $this->normalize_path( $r->target_url )
					&& $this->normalize_path( $other->target_url ) === $this->normalize_path( $r->source_url ) ) {
					++$count;
					break;
				}
			}
		}

		return $count;
	}

	/**
	 * Add a redirection
	 *
	 * @param string $source    Source URL.
	 * @param string $target    Target URL.
	 * @param bool   $is_regex  Is regex pattern.
	 * @return int|false Insert ID or false.
	 */
	private function add_redirection( $source, $target, $is_regex = false ) {
		global $wpdb;

		// Disable if loop detected (non-regex only).
		$is_active = 1;
		if ( ! $is_regex && $this->is_loop_redirection( $source, $target ) ) {
			$is_active = 0;
		}

		$result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$this->table_name,
			array(
				'source_url' => $source,
				'target_url' => $target,
				'is_regex'   => $is_regex ? 1 : 0,
				'is_active'  => $is_active,
			),
			array( '%s', '%s', '%d', '%d' )
		);

		if ( $result ) {
			$this->notify_cache_changed();
		}

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Notify the cache layer that redirection rules have changed so any
	 * cached frontend pages reflecting the old rules get invalidated.
	 */
	private function notify_cache_changed() {
		do_action( 'blaminhor_essentials_cache_purge_request' );
	}

	/**
	 * Public API: create a redirection with the same validation and loop
	 * detection pipeline as the admin AJAX form. Returns WP_Error on
	 * invalid input / duplicate source / DB failure so callers (WP-CLI,
	 * future REST endpoints) can surface a precise error.
	 *
	 * @param string $source   Source URL or regex pattern.
	 * @param string $target   Target URL.
	 * @param bool   $is_regex Treat source as a regex.
	 * @return int|WP_Error Insert ID on success, WP_Error on failure.
	 */
	public function create_redirection( $source, $target, $is_regex = false ) {
		global $wpdb;

		$source = sanitize_text_field( $source );
		$target = sanitize_text_field( $target );

		if ( '' === $source || '' === $target ) {
			return new WP_Error( 'redirections_invalid', __( 'Source and target URLs are required.', 'blaminhor-essentials' ) );
		}

		if ( $is_regex && false === @preg_match( $source, '' ) ) {
			return new WP_Error( 'redirections_invalid_regex', __( 'Invalid regex pattern.', 'blaminhor-essentials' ) );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$exists = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE source_url = %s', $this->table_name, $source ) );
		if ( (int) $exists > 0 ) {
			return new WP_Error( 'redirections_duplicate', __( 'A redirection with this source URL already exists.', 'blaminhor-essentials' ) );
		}

		$id = $this->add_redirection( $source, $target, (bool) $is_regex );
		if ( ! $id ) {
			return new WP_Error( 'redirections_db_error', __( 'Failed to insert redirection.', 'blaminhor-essentials' ) );
		}
		return (int) $id;
	}

	/**
	 * Public API: list redirections (optionally filtered).
	 *
	 * @param array $args {
	 *     Optional.
	 *     @type string $search       Substring to match against source_url or target_url.
	 *     @type bool   $active_only  Only return active redirections.
	 *     @type int    $limit        Maximum number of rows. 0 = no limit.
	 * }
	 * @return array Indexed list of row objects (id, source_url, target_url, is_regex, is_active, hit_count, created_at).
	 */
	public function list_redirections( $args = array() ) {
		global $wpdb;

		$defaults = array( 'search' => '', 'active_only' => false, 'limit' => 0 );
		$args     = wp_parse_args( $args, $defaults );

		$where  = array( '1=1' );
		$params = array( $this->table_name );

		if ( '' !== (string) $args['search'] ) {
			$where[]  = '(source_url LIKE %s OR target_url LIKE %s)';
			$like     = '%' . $wpdb->esc_like( (string) $args['search'] ) . '%';
			$params[] = $like;
			$params[] = $like;
		}

		if ( ! empty( $args['active_only'] ) ) {
			$where[] = 'is_active = 1';
		}

		$limit_sql = '';
		if ( (int) $args['limit'] > 0 ) {
			$limit_sql = ' LIMIT %d';
			$params[]  = (int) $args['limit'];
		}

		$sql = 'SELECT id, source_url, target_url, is_regex, is_active, hit_count, created_at FROM %i WHERE '
			. implode( ' AND ', $where )
			. ' ORDER BY id DESC' . $limit_sql;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared
		return (array) $wpdb->get_results( $wpdb->prepare( $sql, $params ) );
	}

	/**
	 * Public API: delete a redirection by ID. Triggers cache invalidation.
	 *
	 * @param int $id Redirection row ID.
	 * @return true|WP_Error True on success, WP_Error if not found / DB failure.
	 */
	public function delete_redirection( $id ) {
		global $wpdb;

		$id = (int) $id;
		if ( $id <= 0 ) {
			return new WP_Error( 'redirections_invalid_id', __( 'Invalid redirection ID.', 'blaminhor-essentials' ) );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$exists = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE id = %d', $this->table_name, $id ) );
		if ( ! (int) $exists ) {
			return new WP_Error( 'redirections_not_found', __( 'Redirection not found.', 'blaminhor-essentials' ) );
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
		$result = $wpdb->delete( $this->table_name, array( 'id' => $id ), array( '%d' ) );
		if ( false === $result ) {
			return new WP_Error( 'redirections_db_error', __( 'Failed to delete redirection.', 'blaminhor-essentials' ) );
		}

		$this->notify_cache_changed();
		return true;
	}

	/**
	 * AJAX: Add redirection
	 */
	public function ajax_add_redirection() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		$source   = isset( $_POST['source_url'] ) ? sanitize_text_field( wp_unslash( $_POST['source_url'] ) ) : '';
		$target   = isset( $_POST['target_url'] ) ? sanitize_text_field( wp_unslash( $_POST['target_url'] ) ) : '';
		$is_regex = isset( $_POST['is_regex'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['is_regex'] ) );

		if ( empty( $source ) || empty( $target ) ) {
			wp_send_json_error( __( 'Source and target URLs are required.', 'blaminhor-essentials' ) );
		}

		// Validate regex if needed.
		if ( $is_regex ) {
			if ( @preg_match( $source, '' ) === false ) {
				wp_send_json_error( __( 'Invalid regex pattern.', 'blaminhor-essentials' ) );
			}
		}

		// Check for duplicate source URL.
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$exists = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT COUNT(*) FROM %i WHERE source_url = %s',
				$this->table_name,
				$source
			)
		);
		if ( $exists > 0 ) {
			wp_send_json_error( __( 'A redirection with this source URL already exists.', 'blaminhor-essentials' ) );
		}

		// Check for loop.
		$is_loop = ! $is_regex && $this->is_loop_redirection( $source, $target );

		$id = $this->add_redirection( $source, $target, $is_regex );

		if ( $id ) {
			$data = array(
				'message' => __( 'Redirection added.', 'blaminhor-essentials' ),
				'id'      => $id,
			);

			if ( $is_loop ) {
				$data['warning'] = __( 'This redirection creates a loop and has been automatically disabled.', 'blaminhor-essentials' );
			}

			wp_send_json_success( $data );
		} else {
			wp_send_json_error( __( 'Failed to add redirection.', 'blaminhor-essentials' ) );
		}
	}

	/**
	 * AJAX: Edit redirection
	 */
	public function ajax_edit_redirection() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		global $wpdb;

		$id       = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
		$source   = isset( $_POST['source_url'] ) ? sanitize_text_field( wp_unslash( $_POST['source_url'] ) ) : '';
		$target   = isset( $_POST['target_url'] ) ? sanitize_text_field( wp_unslash( $_POST['target_url'] ) ) : '';
		$is_regex = isset( $_POST['is_regex'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['is_regex'] ) );

		if ( ! $id || empty( $source ) || empty( $target ) ) {
			wp_send_json_error( __( 'Invalid data.', 'blaminhor-essentials' ) );
		}

		// Validate regex if needed.
		if ( $is_regex ) {
			if ( @preg_match( $source, '' ) === false ) {
				wp_send_json_error( __( 'Invalid regex pattern.', 'blaminhor-essentials' ) );
			}
		}

		// Check for loop.
		$is_loop   = ! $is_regex && $this->is_loop_redirection( $source, $target, $id );
		$is_active = $is_loop ? 0 : 1;

		$result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$this->table_name,
			array(
				'source_url' => $source,
				'target_url' => $target,
				'is_regex'   => $is_regex ? 1 : 0,
				'is_active'  => $is_active,
			),
			array( 'id' => $id ),
			array( '%s', '%s', '%d', '%d' ),
			array( '%d' )
		);

		if ( false !== $result ) {
			$this->notify_cache_changed();
			$data = array( 'message' => __( 'Redirection updated.', 'blaminhor-essentials' ) );

			if ( $is_loop ) {
				$data['warning'] = __( 'This redirection creates a loop and has been automatically disabled.', 'blaminhor-essentials' );
			}

			wp_send_json_success( $data );
		} else {
			wp_send_json_error( __( 'Failed to update redirection.', 'blaminhor-essentials' ) );
		}
	}

	/**
	 * AJAX: Delete redirection
	 */
	public function ajax_delete_redirection() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		global $wpdb;

		$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;

		if ( ! $id ) {
			wp_send_json_error( __( 'Invalid ID.', 'blaminhor-essentials' ) );
		}

		$result = $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$this->table_name,
			array( 'id' => $id ),
			array( '%d' )
		);

		if ( $result ) {
			$this->notify_cache_changed();
			wp_send_json_success( array( 'message' => __( 'Redirection deleted.', 'blaminhor-essentials' ) ) );
		} else {
			wp_send_json_error( __( 'Failed to delete redirection.', 'blaminhor-essentials' ) );
		}
	}

	/**
	 * AJAX: Toggle redirection
	 */
	public function ajax_toggle_redirection() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		global $wpdb;

		$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;

		if ( ! $id ) {
			wp_send_json_error( __( 'Invalid ID.', 'blaminhor-essentials' ) );
		}

		// Get current status.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for status check
		$current = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT is_active FROM %i WHERE id = %d',
				$this->table_name,
				$id
			)
		);

		$new_status = $current ? 0 : 1;

		// Warn if enabling a loop redirection.
		$warning = '';
		if ( 1 === $new_status ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					'SELECT source_url, target_url, is_regex FROM %i WHERE id = %d',
					$this->table_name,
					$id
				)
			);
			if ( $row && ! $row->is_regex && $this->is_loop_redirection( $row->source_url, $row->target_url, $id ) ) {
				$warning = __( 'Warning: this redirection creates a loop. Enabling it may cause issues.', 'blaminhor-essentials' );
			}
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct update needed for status toggle
		$result = $wpdb->update(
			$this->table_name,
			array( 'is_active' => $new_status ),
			array( 'id' => $id ),
			array( '%d' ),
			array( '%d' )
		);

		if ( false !== $result ) {
			$this->notify_cache_changed();
			$data = array(
				'message'   => __( 'Status updated.', 'blaminhor-essentials' ),
				'is_active' => $new_status,
			);
			if ( $warning ) {
				$data['warning'] = $warning;
			}
			wp_send_json_success( $data );
		} else {
			wp_send_json_error( __( 'Failed to update status.', 'blaminhor-essentials' ) );
		}
	}

	/**
	 * AJAX: Bulk delete redirections
	 */
	public function ajax_bulk_delete() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		$ids = isset( $_POST['ids'] ) ? array_map( 'absint', wp_unslash( (array) $_POST['ids'] ) ) : array();
		$ids = array_filter( $ids );

		if ( empty( $ids ) ) {
			wp_send_json_error( __( 'No redirections selected.', 'blaminhor-essentials' ) );
		}

		global $wpdb;

		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$deleted = $wpdb->query(
			$wpdb->prepare(
				// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
				"DELETE FROM {$this->table_name} WHERE id IN ($placeholders)",
				$ids
			)
		);

		if ( $deleted ) {
			$this->notify_cache_changed();
		}

		wp_send_json_success(
			array(
				/* translators: %d: number of redirections deleted */
				'message' => sprintf( __( '%d redirections deleted.', 'blaminhor-essentials' ), $deleted ),
				'deleted' => $deleted,
			)
		);
	}

	/**
	 * AJAX: Import CSV
	 */
	public function ajax_import_csv() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		$csv_content = isset( $_POST['csv_content'] ) ? sanitize_textarea_field( wp_unslash( $_POST['csv_content'] ) ) : '';

		if ( empty( $csv_content ) ) {
			wp_send_json_error( __( 'No CSV content provided.', 'blaminhor-essentials' ) );
		}

		global $wpdb;

		$lines    = explode( "\n", $csv_content );
		$imported = 0;
		$skipped  = 0;
		$errors   = 0;

		foreach ( $lines as $line ) {
			$line = trim( $line );
			if ( empty( $line ) ) {
				continue;
			}

			// Skip header row.
			if ( stripos( $line, 'source' ) !== false && stripos( $line, 'target' ) !== false ) {
				continue;
			}

			// Parse CSV line.
			$parts = str_getcsv( $line );
			if ( count( $parts ) < 2 ) {
				$errors++;
				continue;
			}

			$source   = trim( $parts[0] );
			$target   = trim( $parts[1] );
			$is_regex = isset( $parts[2] ) && in_array( strtolower( trim( $parts[2] ) ), array( '1', 'true', 'yes', 'regex' ), true );

			if ( empty( $source ) || empty( $target ) ) {
				$errors++;
				continue;
			}

			// Validate regex.
			if ( $is_regex && @preg_match( $source, '' ) === false ) {
				$errors++;
				continue;
			}

			// Check for duplicate source URL.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$exists = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COUNT(*) FROM %i WHERE source_url = %s',
					$this->table_name,
					$source
				)
			);
			if ( $exists > 0 ) {
				$skipped++;
				continue;
			}

			$id = $this->add_redirection( $source, $target, $is_regex );
			if ( $id ) {
				$imported++;
			} else {
				$errors++;
			}
		}

		wp_send_json_success(
			array(
				/* translators: %d: number of redirections imported */
				'message'  => sprintf( __( '%d redirections imported.', 'blaminhor-essentials' ), $imported ),
				'imported' => $imported,
				'skipped'  => $skipped,
				'errors'   => $errors,
			)
		);
	}

	/**
	 * AJAX: Get redirections
	 */
	public function ajax_get_redirections() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

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

		global $wpdb;

		$page     = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
		$per_page = 20;
		$offset   = ( $page - 1 ) * $per_page;
		$search   = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '';

		$table = $this->table_name;

		if ( ! empty( $search ) ) {
			$like_search = '%' . $wpdb->esc_like( $search ) . '%';
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$total = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COUNT(*) FROM %i WHERE source_url LIKE %s OR target_url LIKE %s',
					$table,
					$like_search,
					$like_search
				)
			);
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$redirections = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT * FROM %i WHERE source_url LIKE %s OR target_url LIKE %s ORDER BY id DESC LIMIT %d OFFSET %d',
					$table,
					$like_search,
					$like_search,
					$per_page,
					$offset
				)
			);
		} else {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$total = $wpdb->get_var(
				$wpdb->prepare(
					'SELECT COUNT(*) FROM %i',
					$table
				)
			);
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$redirections = $wpdb->get_results(
				$wpdb->prepare(
					'SELECT * FROM %i ORDER BY id DESC LIMIT %d OFFSET %d',
					$table,
					$per_page,
					$offset
				)
			);
		}

		// Flag loop redirections.
		foreach ( $redirections as $r ) {
			$r->is_loop = ( ! $r->is_regex && $this->is_loop_redirection( $r->source_url, $r->target_url, $r->id ) ) ? 1 : 0;
		}

		wp_send_json_success(
			array(
				'redirections'       => $redirections,
				'total'              => (int) $total,
				'pages'              => ceil( $total / $per_page ),
				'current_page'       => $page,
				'total_redirections' => $this->get_redirections_count(),
				'active_count'       => $this->get_active_redirections_count(),
				'total_hits'         => $this->get_total_hits(),
				'loop_count'         => $this->get_loop_redirections_count(),
			)
		);
	}

	/**
	 * Get redirections count
	 *
	 * @return int
	 */
	private function get_redirections_count() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is internally set and safe
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->table_name}" );
	}

	/**
	 * Get active redirections count
	 *
	 * @return int
	 */
	private function get_active_redirections_count() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is internally set and safe
		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$this->table_name} WHERE is_active = 1" );
	}

	/**
	 * Get total hits
	 *
	 * @return int
	 */
	private function get_total_hits() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name is internally set and safe
		return (int) $wpdb->get_var( "SELECT SUM(hit_count) FROM {$this->table_name}" );
	}

	/**
	 * Render settings page
	 */
	public function render_settings_page() {
		$total_redirections  = $this->get_redirections_count();
		$active_redirections = $this->get_active_redirections_count();
		$total_hits          = $this->get_total_hits();
		?>
		<div class="wrap blaminhor-essentials-wrap">
			<div class="blaminhor-essentials-header">
				<h1><?php esc_html_e( 'Redirections 301', 'blaminhor-essentials' ); ?></h1>
				<p class="blaminhor-essentials-tagline">
					<?php esc_html_e( 'Manage 301 redirections with simple rules or regex patterns.', 'blaminhor-essentials' ); ?>
				</p>
			</div>

			<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="redirections"><?php esc_html_e( 'Redirections', 'blaminhor-essentials' ); ?></button>
						<button type="button" class="blaminhor-essentials-tab" data-tab="add"><?php esc_html_e( 'Add New', 'blaminhor-essentials' ); ?></button>
						<button type="button" class="blaminhor-essentials-tab" data-tab="import"><?php esc_html_e( 'Import', 'blaminhor-essentials' ); ?></button>
						<button type="button" class="blaminhor-essentials-tab" data-tab="settings"><?php esc_html_e( 'Settings', 'blaminhor-essentials' ); ?></button>
					</div>

					<!-- Summary Cards -->
					<div class="ap-db-summary-cards" style="margin-top: 20px; margin-bottom: 24px;">
						<div class="ap-db-card ap-db-card-primary">
							<div class="ap-db-card-icon">
								<span class="dashicons dashicons-admin-links"></span>
							</div>
							<div class="ap-db-card-value" id="ap-total-redirections"><?php echo esc_html( $total_redirections ); ?></div>
							<div class="ap-db-card-label"><?php esc_html_e( 'Total Redirections', 'blaminhor-essentials' ); ?></div>
						</div>
						<div class="ap-db-card ap-db-card-success">
							<div class="ap-db-card-icon">
								<span class="dashicons dashicons-yes-alt"></span>
							</div>
							<div class="ap-db-card-value" id="ap-active-redirections"><?php echo esc_html( $active_redirections ); ?></div>
							<div class="ap-db-card-label"><?php esc_html_e( 'Active', 'blaminhor-essentials' ); ?></div>
						</div>
						<div class="ap-db-card ap-db-card-info">
							<div class="ap-db-card-icon">
								<span class="dashicons dashicons-chart-bar"></span>
							</div>
							<div class="ap-db-card-value" id="ap-total-hits"><?php echo esc_html( number_format_i18n( $total_hits ) ); ?></div>
							<div class="ap-db-card-label"><?php esc_html_e( 'Total Hits', 'blaminhor-essentials' ); ?></div>
						</div>
					</div>

					<!-- Redirections Tab -->
					<div class="blaminhor-essentials-tab-content active" data-tab-content="redirections">
						<div class="ap-flex-center ap-gap-md ap-mb-md">
							<input type="text" id="ap-redirections-search" class="regular-text" placeholder="<?php esc_attr_e( 'Search redirections...', 'blaminhor-essentials' ); ?>">
							<button type="button" id="ap-redirections-refresh" class="button button-secondary">
								<span class="dashicons dashicons-update ap-mt-xs"></span>
							</button>
						</div>

						<div class="ap-bulk-actions" id="ap-redirections-bulk-actions">
							<span class="ap-bulk-count"></span>
							<button type="button" class="button button-small ap-bulk-delete-btn ap-redirections-bulk-delete-btn">
								<span class="dashicons dashicons-trash ap-icon-16" style="margin-top: 2px; margin-right: 2px;"></span>
								<?php esc_html_e( 'Delete Selected', 'blaminhor-essentials' ); ?>
							</button>
						</div>

						<table class="wp-list-table widefat fixed striped" id="ap-redirections-table">
							<thead>
								<tr>
									<th class="ap-col-checkbox"><input type="checkbox" id="ap-redirections-select-all" title="<?php esc_attr_e( 'Select All', 'blaminhor-essentials' ); ?>"></th>
									<th style="width: 32%;"><?php esc_html_e( 'Source URL', 'blaminhor-essentials' ); ?></th>
									<th style="width: 32%;"><?php esc_html_e( 'Target URL', 'blaminhor-essentials' ); ?></th>
									<th style="width: 8%;"><?php esc_html_e( 'Type', 'blaminhor-essentials' ); ?></th>
									<th style="width: 7%;"><?php esc_html_e( 'Hits', 'blaminhor-essentials' ); ?></th>
									<th style="width: 15%;"><?php esc_html_e( 'Actions', 'blaminhor-essentials' ); ?></th>
								</tr>
							</thead>
							<tbody id="ap-redirections-tbody">
								<tr>
									<td colspan="6" class="ap-table-empty">
										<span class="dashicons dashicons-update spin"></span>
										<?php esc_html_e( 'Loading...', 'blaminhor-essentials' ); ?>
									</td>
								</tr>
							</tbody>
						</table>

						<div id="ap-redirections-pagination" class="ap-pagination"></div>
					</div>

					<!-- Add New Tab -->
					<div class="blaminhor-essentials-tab-content" data-tab-content="add">
						<h3><?php esc_html_e( 'Add New Redirection', 'blaminhor-essentials' ); ?></h3>

						<table class="form-table">
							<tr>
								<th scope="row">
									<label for="ap-add-source"><?php esc_html_e( 'Source URL', 'blaminhor-essentials' ); ?></label>
								</th>
								<td>
									<input type="text" id="ap-add-source" class="large-text" placeholder="/old-page/">
									<p class="description"><?php esc_html_e( 'The URL path to redirect from. Example: /old-page/', 'blaminhor-essentials' ); ?></p>
								</td>
							</tr>
							<tr>
								<th scope="row">
									<label for="ap-add-target"><?php esc_html_e( 'Target URL', 'blaminhor-essentials' ); ?></label>
								</th>
								<td>
									<input type="text" id="ap-add-target" class="large-text" placeholder="/new-page/">
									<p class="description"><?php esc_html_e( 'The URL to redirect to. Can be a path or full URL.', 'blaminhor-essentials' ); ?></p>
								</td>
							</tr>
							<tr>
								<th scope="row"><?php esc_html_e( 'Type', 'blaminhor-essentials' ); ?></th>
								<td>
									<label>
										<input type="checkbox" id="ap-add-regex">
										<?php esc_html_e( 'Use regular expression (regex)', 'blaminhor-essentials' ); ?>
									</label>
									<p class="description"><?php esc_html_e( 'Enable to use regex patterns for advanced matching.', 'blaminhor-essentials' ); ?></p>
								</td>
							</tr>
						</table>

						<p>
							<button type="button" id="ap-add-redirection-btn" class="button button-primary">
								<?php esc_html_e( 'Add Redirection', 'blaminhor-essentials' ); ?>
							</button>
						</p>

						<div id="ap-add-result" class="ap-hidden ap-mt-md"></div>
					</div>

					<!-- Import Tab -->
					<div class="blaminhor-essentials-tab-content" data-tab-content="import">
						<h3><?php esc_html_e( 'Import from Plugins', 'blaminhor-essentials' ); ?></h3>
						<p class="description"><?php esc_html_e( 'Import redirections from other installed plugins.', 'blaminhor-essentials' ); ?></p>

						<?php
						$import_sources = $this->get_available_import_sources();
						if ( ! empty( $import_sources ) ) :
						?>

						<div class="ap-import-sources">
							<?php foreach ( $import_sources as $source_id => $source ) : ?>
								<div class="ap-import-source-card">
									<div class="ap-import-source-layout">
										<div class="ap-import-source-info">
											<h4 class="ap-import-source-title">
												<span class="dashicons dashicons-randomize ap-icon-mr"></span>
												<?php echo esc_html( $source['name'] ); ?>
											</h4>
											<p class="description">
												<?php
												printf(
													/* translators: %d: number of redirections */
													esc_html__( '%d redirections found', 'blaminhor-essentials' ),
													intval( $source['count'] )
												);
												?>
											</p>
										</div>
										<div class="ap-import-source-actions">
											<button type="button" class="button button-primary ap-import-plugin-btn"
												data-source="<?php echo esc_attr( $source_id ); ?>"
												data-count="<?php echo esc_attr( $source['count'] ); ?>">
												<?php esc_html_e( 'Import', 'blaminhor-essentials' ); ?>
											</button>
										</div>
									</div>
									<div class="ap-import-options">
										<label class="ap-label-flex">
											<input type="checkbox" class="ap-import-skip-duplicates" data-source="<?php echo esc_attr( $source_id ); ?>" checked>
											<?php esc_html_e( 'Skip duplicates', 'blaminhor-essentials' ); ?>
										</label>
									</div>
									<div class="ap-import-plugin-progress ap-hidden ap-mt-md" data-source="<?php echo esc_attr( $source_id ); ?>">
										<div class="ap-mini-progress-bar">
											<div class="ap-import-progress-bar ap-mini-progress-fill" style="width: 0%;"></div>
										</div>
										<p class="ap-import-status"></p>
									</div>
									<div class="ap-import-plugin-result ap-hidden ap-mt-md" data-source="<?php echo esc_attr( $source_id ); ?>"></div>
								</div>
							<?php endforeach; ?>
						</div>

						<hr>
						<?php else : ?>
						<div class="ap-notice ap-notice-info ap-mb-lg">
							<p><?php esc_html_e( 'No compatible redirection plugins detected. Supported plugins: Redirection, Safe Redirect Manager.', 'blaminhor-essentials' ); ?></p>
						</div>
						<?php endif; ?>

						<h3><?php esc_html_e( 'Import from CSV', 'blaminhor-essentials' ); ?></h3>
						<p class="description"><?php esc_html_e( 'Paste CSV content or upload a CSV file. Format: source_url,target_url,is_regex (optional)', 'blaminhor-essentials' ); ?></p>

						<table class="form-table">
							<tr>
								<th scope="row">
									<label for="ap-import-csv"><?php esc_html_e( 'CSV Content', 'blaminhor-essentials' ); ?></label>
								</th>
								<td>
									<textarea id="ap-import-csv" class="large-text" rows="10" placeholder="/old-page/,/new-page/
/old-post/,/new-post/
#^/category/(.*)$#,/cat/$1,regex"></textarea>
									<p class="description">
										<?php esc_html_e( 'One redirection per line. Third column (optional): "regex", "1", "true", or "yes" to mark as regex.', 'blaminhor-essentials' ); ?>
									</p>
								</td>
							</tr>
							<tr>
								<th scope="row">
									<label for="ap-import-file"><?php esc_html_e( 'Or Upload File', 'blaminhor-essentials' ); ?></label>
								</th>
								<td>
									<input type="file" id="ap-import-file" accept=".csv,.txt">
								</td>
							</tr>
						</table>

						<p>
							<button type="button" id="ap-import-btn" class="button button-primary">
								<?php esc_html_e( 'Import Redirections', 'blaminhor-essentials' ); ?>
							</button>
						</p>

						<div id="ap-import-result" class="ap-hidden ap-mt-md"></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( 'Automatic Redirects', 'blaminhor-essentials' ); ?></th>
									<td>
										<label>
											<input type="checkbox" name="auto_redirect_on_slug_change" <?php checked( $this->get_setting( 'auto_redirect_on_slug_change', true ) ); ?>>
											<?php esc_html_e( 'Propose to add redirect when a post or taxonomy slug changes', 'blaminhor-essentials' ); ?>
										</label>
										<p class="description"><?php esc_html_e( 'When enabled, a notice will appear after changing a slug, allowing you to add a 301 redirect.', 'blaminhor-essentials' ); ?></p>
									</td>
								</tr>
								<tr>
									<th scope="row"><?php esc_html_e( 'Matching', 'blaminhor-essentials' ); ?></th>
									<td>
										<label>
											<input type="checkbox" name="case_insensitive" <?php checked( $this->get_setting( 'case_insensitive', true ) ); ?>>
											<?php esc_html_e( 'Case insensitive URL matching', '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>

		<!-- Edit Modal -->
		<div id="ap-edit-modal" class="ap-db-modal ap-hidden">
			<div class="ap-db-modal-overlay"></div>
			<div class="ap-db-modal-content" style="max-width: 500px;">
				<div class="ap-db-modal-header">
					<h3><?php esc_html_e( 'Edit Redirection', 'blaminhor-essentials' ); ?></h3>
					<button type="button" class="ap-modal-close">&times;</button>
				</div>
				<div class="ap-db-modal-body">
					<input type="hidden" id="ap-edit-id">
					<table class="form-table" style="margin: 0;">
						<tr>
							<th scope="row"><label for="ap-edit-source"><?php esc_html_e( 'Source URL', 'blaminhor-essentials' ); ?></label></th>
							<td><input type="text" id="ap-edit-source" class="large-text"></td>
						</tr>
						<tr>
							<th scope="row"><label for="ap-edit-target"><?php esc_html_e( 'Target URL', 'blaminhor-essentials' ); ?></label></th>
							<td><input type="text" id="ap-edit-target" class="large-text"></td>
						</tr>
						<tr>
							<th scope="row"><?php esc_html_e( 'Type', 'blaminhor-essentials' ); ?></th>
							<td>
								<label>
									<input type="checkbox" id="ap-edit-regex">
									<?php esc_html_e( 'Regular expression', 'blaminhor-essentials' ); ?>
								</label>
							</td>
						</tr>
					</table>
				</div>
				<div class="ap-db-modal-footer">
					<button type="button" class="button button-secondary ap-modal-close"><?php esc_html_e( 'Cancel', 'blaminhor-essentials' ); ?></button>
					<button type="button" id="ap-edit-save" class="button button-primary"><?php esc_html_e( 'Save Changes', 'blaminhor-essentials' ); ?></button>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Get available import sources
	 *
	 * Detects installed redirection plugins with importable data.
	 *
	 * @return array Array of available import sources with counts.
	 */
	private function get_available_import_sources() {
		global $wpdb;

		$sources = array();

		// Redirection plugin detection.
		$redirection_table = $wpdb->prefix . 'redirection_items';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time import detection query.
		$table_exists      = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $redirection_table ) );

		if ( $table_exists ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table name is safe (uses $wpdb->prefix), one-time import query.
			$count = $wpdb->get_var( "SELECT COUNT(*) FROM {$redirection_table} WHERE action_type = 'url'" );
			if ( $count > 0 ) {
				$sources['redirection'] = array(
					'name'  => 'Redirection',
					'count' => intval( $count ),
				);
			}
		}

		// Safe Redirect Manager detection.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time import detection query.
		$srm_count = $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->posts}
			WHERE post_type = 'redirect_rule'
			AND post_status = 'publish'"
		);
		if ( $srm_count > 0 ) {
			$sources['safe_redirect_manager'] = array(
				'name'  => 'Safe Redirect Manager',
				'count' => intval( $srm_count ),
			);
		}

		return $sources;
	}

	/**
	 * AJAX handler for importing from plugins
	 */
	public function ajax_import_from_plugin() {
		check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'blaminhor-essentials' ) ) );
		}

		$source         = isset( $_POST['source'] ) ? sanitize_text_field( wp_unslash( $_POST['source'] ) ) : '';
		$skip_duplicates = isset( $_POST['skip_duplicates'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['skip_duplicates'] ) );
		$offset         = isset( $_POST['offset'] ) ? intval( $_POST['offset'] ) : 0;
		$batch          = 50;

		global $wpdb;

		$redirections = array();
		$total        = 0;

		if ( 'redirection' === $source ) {
			$redirection_table = $wpdb->prefix . 'redirection_items';
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$total = $wpdb->get_var( "SELECT COUNT(*) FROM {$redirection_table} WHERE action_type = 'url'" );

			$items = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT url, action_data, regex, status FROM {$redirection_table} WHERE action_type = 'url' LIMIT %d, %d",
					$offset,
					$batch
				)
			);
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter

			foreach ( $items as $item ) {
				$redirections[] = array(
					'source_url' => $item->url,
					'target_url' => $item->action_data,
					'is_regex'   => (bool) $item->regex,
					'is_active'  => 'enabled' === $item->status,
				);
			}
		} elseif ( 'safe_redirect_manager' === $source ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time import query.
			$total = $wpdb->get_var(
				"SELECT COUNT(*) FROM {$wpdb->posts}
				WHERE post_type = 'redirect_rule'
				AND post_status = 'publish'"
			);

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-time import query.
			$posts = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT ID FROM {$wpdb->posts}
					WHERE post_type = 'redirect_rule'
					AND post_status = 'publish'
					ORDER BY ID ASC
					LIMIT %d, %d",
					$offset,
					$batch
				)
			);

			foreach ( $posts as $post ) {
				$from = get_post_meta( $post->ID, '_redirect_rule_from', true );
				$to   = get_post_meta( $post->ID, '_redirect_rule_to', true );

				if ( $from && $to ) {
					$redirections[] = array(
						'source_url' => $from,
						'target_url' => $to,
						'is_regex'   => false,
						'is_active'  => true,
					);
				}
			}
		} else {
			wp_send_json_error( array( 'message' => __( 'Invalid import source.', 'blaminhor-essentials' ) ) );
		}

		$imported = 0;
		$skipped  = 0;

		foreach ( $redirections as $redirection ) {
			// Check for duplicates.
			if ( $skip_duplicates ) {
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$exists = $wpdb->get_var(
					$wpdb->prepare(
						"SELECT COUNT(*) FROM {$this->table_name} WHERE source_url = %s",
						$redirection['source_url']
					)
				);
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				if ( $exists > 0 ) {
					++$skipped;
					continue;
				}
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Import operation.
			$result = $wpdb->insert(
				$this->table_name,
				array(
					'source_url' => $redirection['source_url'],
					'target_url' => $redirection['target_url'],
					'is_regex'   => $redirection['is_regex'] ? 1 : 0,
					'is_active'  => $redirection['is_active'] ? 1 : 0,
				),
				array( '%s', '%s', '%d', '%d' )
			);

			if ( $result ) {
				++$imported;
			}
		}

		$processed   = $offset + count( $redirections );
		$has_more    = $processed < $total && count( $redirections ) > 0;
		$next_offset = $has_more ? $processed : 0;

		wp_send_json_success(
			array(
				'imported'    => $imported,
				'skipped'     => $skipped,
				'processed'   => $processed,
				'total'       => intval( $total ),
				'has_more'    => $has_more,
				'next_offset' => $next_offset,
			)
		);
	}
}
