<?php
/**
 * Redirect Manager Module
 *
 * Manages redirects with exact, wildcard, and regex matching.
 * Supports conditions, import/export, and bulk operations.
 *
 * @package RedirectManager
 * @since 1.0.0
 */

namespace RedirectManager;

use WP_PowerSuite\Core\Assets;
use WP_PowerSuite\Core\Contracts\DataLocationsInterface;
use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\DataRegistry;
use WP_PowerSuite\Core\FrontendRequestGuard;
use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;
use WP_PowerSuite\Core\RegexGuard;
use WP_PowerSuite\Core\RequestPathHelper;

class Module extends AbstractModule implements DataLocationsInterface {

	private const MODULE_ID = 'redirect-manager';
	private const MODULE_VERSION = '1.5.0';
	private const TABLE = 'wpps_redirects';
	private const DB_VERSION = 1;
	private const CACHE_KEY = 'wp_powersuite_redirect_rules';
	private const CACHE_TTL = 300;
	private const LOOP_MAX_CHAIN = 8;
	private const IMPORT_MAX_ROWS = 1000;
	private const IMPORT_MAX_BYTES = 1048576;
	private const IMPORT_MAX_FIELD_LENGTH = 2048;
	private const LOOP_CHAIN_COOKIE = 'wpps_redirect_chain';

private string $table_name = '';

	/** @var array<string, int> */
	private array $pending_hit_counts = [];

	private bool $hit_flush_registered = false;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Redirect Manager', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Full redirect manager—exact, pattern, and regex rules, plus bulk and import/export.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'frontend', 'ajax' ];
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}


	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		global $wpdb;
		$this->table_name = $wpdb->prefix . self::TABLE;


		DataRegistry::registerTable( self::TABLE );

		add_action( 'init', [ $this, 'maybeCreateTableAndMigrate' ], 5 );
		add_action( 'wp_loaded', [ $this, 'executeRedirects' ], 1 );

		if ( is_admin() ) {
		add_action( 'admin_menu', [ $this, 'addAdminMenu' ], 20 );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueScripts' ] );
		add_action( 'admin_post_wpps_redirect_manager_save', [ $this, 'handleSave' ] );
		add_action( 'admin_post_wpps_redirect_manager_delete', [ $this, 'handleDelete' ] );
		add_action( 'admin_post_wpps_redirect_manager_bulk', [ $this, 'handleBulk' ] );
		add_action( 'admin_post_wpps_redirect_manager_import', [ $this, 'handleImport' ] );
		add_action( 'admin_post_wpps_redirect_manager_export', [ $this, 'handleExport' ] );
		add_action( 'admin_post_wpps_redirect_manager_clear_cache', [ $this, 'handleClearCache' ] );
		add_action( 'admin_post_wpps_redirect_manager_duplicate', [ $this, 'handleDuplicate' ] );
		add_action( 'wp_ajax_wpps_redirect_manager_toggle', [ $this, 'ajaxToggle' ] );
		add_action( 'wp_ajax_wpps_redirect_manager_get_pages', [ $this, 'ajaxGetPages' ] );
		add_action( 'wp_ajax_wpps_redirect_manager_test_url', [ $this, 'ajaxTestUrl' ] );
		}
	}


	/**
	 * Get table name, ensuring it is set (handles fresh Module instances).
	 *
	 * @return string
	 */
	private function getTableName(): string {
		if ( $this->table_name === '' ) {
			global $wpdb;
			$this->table_name = $wpdb->prefix . self::TABLE;
		}
		return $this->table_name;
	}

	public function getDataLocations(): array {
		return [
			'tables' => [ self::TABLE ],
			'options' => [ 'wpps_redirects_db_version' ],
		];
	}

	/**
	 * Create table if needed and migrate from options.
	 */
	public function maybeCreateTableAndMigrate(): void {
		$installed = (int) get_option( 'wpps_redirects_db_version', 0 );
		if ( $installed >= self::DB_VERSION ) {
			return;
		}

		$this->createTable();
		$this->migrateFromOptions();
		update_option( 'wpps_redirects_db_version', self::DB_VERSION, false );
	}

	/**
	 * Normalize source URL to path format for matching REQUEST_URI.
	 * Full URLs (https://example.com/about/) become paths (/about).
	 * Trailing slashes are stripped (except homepage "/") for consistent matching.
	 *
	 * @param string $source Source URL or path.
	 * @return string Path like /about-us
	 */
	private function normalizeSourceToPath( string $source ): string {
		$source = trim( $source );
		if ( preg_match( '#^https?://#i', $source ) ) {
			$parsed = parse_url( $source );
			$source = isset( $parsed['path'] ) && $parsed['path'] !== '' ? $parsed['path'] : '/';
		}
		$path = '/' . ltrim( $source, '/' );
		if ( $path !== '/' ) {
			$path = rtrim( $path, '/' );
		}

		return $path === '' ? '/' : $path;
	}

	/**
	 * @return list<string>|true|false Captures, true on match with no captures, false on no match.
	 */
	private function matchRule( string $path, array $rule ) {
		$source     = $rule['source'] ?? '';
		$match_type = $rule['match_type'] ?? 'exact';

		if ( $source === '' ) {
			return false;
		}

		$path   = rtrim( $path, '/' ) ?: '/';
		$source = rtrim( $source, '/' ) ?: '/';

		if ( $match_type === 'regex' ) {
			$matches = [];
			if ( RegexGuard::match( $source, $path, $matches ) ) {
				$captures = array_slice( $matches, 1 );

				// Empty array is falsy in PHP — return true so callers using === false work.
				return $captures === [] ? true : $captures;
			}
			return false;
		}

		if ( $match_type === 'wildcard' ) {
			$include_base = str_ends_with( $source, '/*' );
			$pattern      = str_replace( '\*', '(.*)', preg_quote( $source, '/' ) );
			$pattern      = '/^' . $pattern . '$/i';
			if ( preg_match( $pattern, $path, $m ) ) {
				$captures = array_slice( $m, 1 );

				return $captures === [] ? true : $captures;
			}
			// /blog/* also matches the base path /blog (and /blog/).
			if ( $include_base ) {
				$base = substr( $source, 0, -2 );
				$base = rtrim( $base, '/' ) ?: '/';
				if ( strcasecmp( $path, $base ) === 0 ) {
					$star_count = substr_count( $source, '*' );

					return $star_count > 0 ? array_fill( 0, $star_count, '' ) : true;
				}
			}
			return false;
		}

		return strcasecmp( $path, $source ) === 0 ? true : false;
	}

	/**
	 * @param list<string>|true $match
	 */
	private function resolveTargetUrl( string $target, $match ): string {
		if ( $target === '' ) {
			return '';
		}
		$target_is_absolute = (bool) preg_match( '#^https?://#i', $target );
		if ( is_array( $match ) && $match !== [] ) {
			for ( $i = count( $match ); $i >= 1; $i-- ) {
				$capture = (string) $match[ $i - 1 ];
				if ( ! $target_is_absolute ) {
					// Block open redirects via captures injecting a scheme or protocol-relative URL.
					if ( preg_match( '#^[a-z][a-z0-9+.-]*:#i', $capture ) || str_starts_with( $capture, '//' ) ) {
						$capture = '';
					}
				}
				$target = str_replace( '$' . $i, $capture, $target );
			}
		}
		if ( ! preg_match( '#^https?://#i', $target ) ) {
			$target = home_url( $target );
		}
		return $target;
	}

	/**
	 * Ensure redirects table exists (creates if missing).
	 */
	private function ensureTableExists(): void {
		global $wpdb;
		$table = $this->getTableName();
		$exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
		if ( ! $exists ) {
			$this->createTable();
		}
	}

	private function createTable(): void {
		global $wpdb;
		$charset = $wpdb->get_charset_collate();
		$sql = "CREATE TABLE IF NOT EXISTS {$this->getTableName()} (
			id varchar(36) NOT NULL,
			source_url varchar(500) NOT NULL,
			target_url varchar(500) NOT NULL,
			redirect_type varchar(3) DEFAULT '301',
			match_type varchar(20) DEFAULT 'exact',
			is_active tinyint(1) DEFAULT 1,
			priority int DEFAULT 10,
			notes text,
			strip_params tinyint(1) DEFAULT 0,
			custom_message varchar(500) DEFAULT '',
			conditions_json longtext,
			hit_count bigint(20) UNSIGNED DEFAULT 0,
			last_hit datetime DEFAULT NULL,
			created_at datetime DEFAULT CURRENT_TIMESTAMP,
			PRIMARY KEY (id),
			KEY source_url (source_url(191)),
			KEY is_active (is_active),
			KEY hit_count (hit_count)
		) {$charset};";
		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );
	}

	private function migrateFromOptions(): void {
		$data = $this->moduleSettings()->all( [] );
		$list = isset( $data['redirects'] ) && is_array( $data['redirects'] ) ? $data['redirects'] : [];
		if ( empty( $list ) ) {
			return;
		}

		global $wpdb;
		foreach ( $list as $r ) {
			$this->insertRedirect( $this->rowToDb( $r ) );
		}

		$this->moduleSettings()->save( [ 'redirects' => [] ] );
		$this->clearCache();
	}

	/**
	 * Convert internal row to DB format.
	 *
	 * @param array<string, mixed> $r
	 * @return array<string, mixed>
	 */
	private function rowToDb( array $r ): array {
		$conds = $r['conditions'] ?? [];
		return [
			'id'             => $r['id'] ?? wp_generate_uuid4(),
			'source_url'     => $r['source'] ?? '',
			'target_url'     => $r['target'] ?? '',
			'redirect_type'  => $r['type'] ?? '301',
			'match_type'     => $r['match_type'] ?? 'exact',
			'is_active'      => ! empty( $r['active'] ) ? 1 : 0,
			'priority'       => (int) ( $r['priority'] ?? 10 ),
			'notes'          => $r['notes'] ?? '',
			'strip_params'   => ! empty( $r['strip_params'] ) ? 1 : 0,
			'custom_message' => $r['message'] ?? '',
			'conditions_json'=> is_array( $conds ) ? wp_json_encode( $conds ) : '[]',
			'hit_count'      => (int) ( $r['hits'] ?? 0 ),
			'last_hit'       => ! empty( $r['last_hit'] ) ? $r['last_hit'] : null,
		];
	}

	/**
	 * Convert DB row to internal format.
	 *
	 * @param object $row
	 * @return array<string, mixed>
	 */
	private function dbToRow( object $row ): array {
		$conds = isset( $row->conditions_json ) && $row->conditions_json !== '' ? json_decode( $row->conditions_json, true ) : [];
		return [
			'id'           => $row->id,
			'source'       => $row->source_url,
			'target'       => $row->target_url,
			'type'         => $row->redirect_type ?? '301',
			'match_type'   => $row->match_type ?? 'exact',
			'active'       => ! empty( $row->is_active ),
			'priority'     => (int) ( $row->priority ?? 10 ),
			'notes'        => $row->notes ?? '',
			'strip_params' => ! empty( $row->strip_params ),
			'message'      => $row->custom_message ?? '',
			'conditions'   => is_array( $conds ) ? $conds : [],
			'hits'         => (int) ( $row->hit_count ?? 0 ),
			'last_hit'     => $row->last_hit ?? null,
		];
	}

	private function insertRedirect( array $db ): void {
		global $wpdb;
		$wpdb->insert(
			$this->getTableName(),
			$db,
			[ '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%s', '%s', '%d', '%s' ]
		);
	}

	public function addAdminMenu(): void {
		add_submenu_page(
			'powersuite',
			__( 'Redirect Manager', 'wp-powersuite' ),
			__( 'Redirects', 'wp-powersuite' ),
			'manage_options',
			'powersuite-redirect-manager',
			[ $this, 'renderAdminPage' ]
		);
	}

	public function renderAdminPage(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-powersuite' ) );
		}
		if ( ! $this->isLicenseActive() ) {
			include __DIR__ . '/admin.php';
			return;
		}
		$bulk_action = isset( $_POST['action'] ) && $_POST['action'] !== '-1' ? sanitize_key( wp_unslash( $_POST['action'] ) ) : ( isset( $_POST['action2'] ) && $_POST['action2'] !== '-1' ? sanitize_key( wp_unslash( $_POST['action2'] ) ) : '' );
		$bulk_ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['ids'] ) ) : [];
		$bulk_ids = array_filter( $bulk_ids );
		if ( $bulk_action && ! empty( $bulk_ids ) && check_admin_referer( 'bulk-redirects' ) ) {
			$this->processBulkAction( $bulk_action, $bulk_ids );
		}
		$module = $this;
		include __DIR__ . '/admin.php';
	}

	/**
	 * Process bulk action (enable, disable, delete).
	 *
	 * @param string   $action Action: enable, disable, delete.
	 * @param string[] $ids    Redirect IDs.
	 */
	private function processBulkAction( string $action, array $ids ): void {
		global $wpdb;
		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%s' ) );
		if ( $action === 'delete' ) {
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$this->getTableName()} WHERE id IN ($placeholders)", $ids ) );
		} elseif ( $action === 'enable' ) {
			$wpdb->query( $wpdb->prepare( "UPDATE {$this->getTableName()} SET is_active = 1 WHERE id IN ($placeholders)", $ids ) );
		} elseif ( $action === 'disable' ) {
			$wpdb->query( $wpdb->prepare( "UPDATE {$this->getTableName()} SET is_active = 0 WHERE id IN ($placeholders)", $ids ) );
		}
		$this->clearCache();
		$redirect = add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'bulk' => '1' ], admin_url( 'admin.php' ) );
		$status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : ( isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : 'all' );
		if ( $status !== 'all' ) {
			$redirect = add_query_arg( 'status', $status, $redirect );
		}
		if ( isset( $_POST['s'] ) && $_POST['s'] !== '' ) {
			$redirect = add_query_arg( 's', sanitize_text_field( wp_unslash( $_POST['s'] ) ), $redirect );
		}
		wp_safe_redirect( $redirect );
		exit;
	}

	public function enqueueScripts( string $hook ): void {
		if ( $hook !== 'wp-powersuite_page_powersuite-redirect-manager' ) {
			return;
		}
		wp_enqueue_style( 'dashicons' );
		wp_enqueue_style( 'wp-powersuite-admin' );
		wp_enqueue_style( 'wpps-redirect-manager', wp_powersuite_module_asset_url( __FILE__, 'assets/redirect-manager.css' ), [ 'wp-powersuite-admin' ], self::MODULE_VERSION );
		$module_settings_ui = defined( 'WP_POWERSUITE_SRC_DIR' )
			? WP_POWERSUITE_SRC_DIR . 'Admin' . DIRECTORY_SEPARATOR . 'ModuleSettingsUi.php'
			: '';
		if ( $module_settings_ui !== '' && is_readable( $module_settings_ui ) ) {
			require_once $module_settings_ui;
			wpps_enqueue_accordion_tooltip_script();
		}
		Assets::enqueueNumberClampScript();
	}

	public function executeRedirects(): void {
		if ( is_admin() ) {
			return;
		}
		if ( FrontendRequestGuard::shouldSkipPublic404Action() ) {
			return;
		}

		$uri  = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$path = RequestPathHelper::stripHomePath( RequestPathHelper::pathFromRequestUri( $uri ) ?: '/' );
		// Allow static asset redirects (e.g. Media Rename); still skip system routes.
		if ( FrontendRequestGuard::isSystemPath( $path ) ) {
			return;
		}
		if ( apply_filters( 'wp_powersuite_skip_404_path', false, $path ) ) {
			return;
		}
		$query = RequestPathHelper::queryFromRequestUri( $uri );

		$rules = $this->getActiveRules();
		if ( empty( $rules ) ) {
			return;
		}

		foreach ( $rules as $rule ) {
			if ( ! $this->checkConditions( $rule ) ) {
				continue;
			}

			$match = $this->matchRule( $path, $rule );
			if ( $match === false ) {
				continue;
			}

			$code = $this->getHttpCode( $rule['type'] ?? '301' );

			if ( $code >= 400 ) {
				if ( $this->isPathOvervisited( $path ) ) {
					continue;
				}
				$this->recordRedirectChain( $path, $path );
				$this->queueHitCount( $rule['id'] );
				$msg = trim( $rule['message'] ?? '' );
				if ( $msg === '' ) {
					$msg = $this->getDefaultErrorMessage( $rule['type'] ?? '404' );
				}
				wp_die( esc_html( $msg ), '', [ 'response' => $code ] );
			}

			$target_url = $this->resolveTargetUrl( $rule['target'], $match );
			if ( $target_url === '' ) {
				continue;
			}

			$strip_params = ! empty( $rule['strip_params'] );
			if ( ! $strip_params && $query !== '' ) {
				$target_url .= ( strpos( $target_url, '?' ) !== false ? '&' : '?' ) . $query;
			}

			$validated = esc_url_raw( $target_url, [ 'http', 'https' ] );
			if ( $validated === '' ) {
				continue;
			}

			$target_parsed = wp_parse_url( $validated );
			$target_path   = is_array( $target_parsed ) ? ( $target_parsed['path'] ?? '' ) : '';
			$target_host   = is_array( $target_parsed ) ? strtolower( (string) ( $target_parsed['host'] ?? '' ) ) : '';
			$home_host     = strtolower( (string) ( wp_parse_url( home_url(), PHP_URL_HOST ) ?: '' ) );
			$same_host     = $target_host === '' || $home_host === '' || $target_host === $home_host;
			// Only treat same-path as a self-loop on the same host (allow cross-domain same path).
			if ( $same_host && rtrim( (string) $target_path, '/' ) === rtrim( $path, '/' ) ) {
				continue;
			}

			if ( $this->isRedirectChainLoop( $path, (string) $target_path ) ) {
				continue;
			}

			$this->recordRedirectChain( $path, (string) $target_path );
			$this->queueHitCount( $rule['id'] );

			do_action( 'wpps_redirect_manager_redirect', $path, $validated, $code, $rule );

			if ( ! $this->performSafeRedirect( $validated, $code ) ) {
				continue;
			}

			exit;
		}
	}

	/**
	 * Redirect without falling back to wp-admin for validated external URLs (C2).
	 */
	private function performSafeRedirect( string $url, int $code ): bool {
		$url = esc_url_raw( $url, [ 'http', 'https' ] );
		if ( $url === '' ) {
			return false;
		}

		$host = wp_parse_url( $url, PHP_URL_HOST );
		if ( ! is_string( $host ) || $host === '' ) {
			return false;
		}

		$allow_host = static function ( array $hosts ) use ( $host ): array {
			$hosts[] = $host;

			return array_values( array_unique( $hosts ) );
		};

		add_filter( 'allowed_redirect_hosts', $allow_host );
		$validated = wp_validate_redirect( $url, '' );
		remove_filter( 'allowed_redirect_hosts', $allow_host );

		if ( $validated === '' ) {
			return false;
		}

		add_filter( 'allowed_redirect_hosts', $allow_host );
		wp_safe_redirect( $validated, $code, 'WP PowerSuite Redirect Manager' );
		remove_filter( 'allowed_redirect_hosts', $allow_host );

		return true;
	}

	/**
	 * @param array{skip_login?: bool, get?: array<string, mixed>}|null $overrides Optional Test URL overrides.
	 */
	private function checkConditions( array $rule, ?array $overrides = null ): bool {
		$conds = $rule['conditions'] ?? [];
		if ( empty( $conds ) ) {
			return true;
		}

		if ( ! empty( $conds['login_status']['enabled'] ) && empty( $overrides['skip_login'] ) ) {
			$want = $conds['login_status']['status'] ?? 'logged_out';
			$logged = is_user_logged_in();
			if ( $want === 'logged_in' && ! $logged ) {
				return false;
			}
			if ( $want === 'logged_out' && $logged ) {
				return false;
			}
		}

		if ( ! empty( $conds['referrer']['enabled'] ) ) {
			$pattern = $conds['referrer']['pattern'] ?? '';
			$referrer = isset( $_SERVER['HTTP_REFERER'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
			if ( $pattern !== '' && ( $referrer === '' || stripos( $referrer, $pattern ) === false ) ) {
				return false;
			}
		}

		if ( ! empty( $conds['url_param']['enabled'] ) ) {
			$name = $conds['url_param']['name'] ?? '';
			$value = $conds['url_param']['value'] ?? '';
			if ( $name === '' ) {
				return false;
			}
			$get = is_array( $overrides['get'] ?? null ) ? $overrides['get'] : $_GET;
			$actual = isset( $get[ $name ] ) ? sanitize_text_field( wp_unslash( (string) $get[ $name ] ) ) : '';
			if ( $value !== '' && $actual !== $value ) {
				return false;
			}
			if ( $actual === '' ) {
				return false;
			}
		}

		if ( ! empty( $conds['language']['enabled'] ) ) {
			$codes = array_map( 'trim', explode( ',', $conds['language']['codes'] ?? '' ) );
			$codes = array_filter( $codes );
			if ( empty( $codes ) ) {
				return false;
			}
			$accept = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : '';
			$lang_match = false;
			foreach ( $codes as $code ) {
				if ( stripos( $accept, $code ) !== false ) {
					$lang_match = true;
					break;
				}
			}
			if ( ! $lang_match ) {
				return false;
			}
		}

		return true;
	}

	private function isPathOvervisited( string $path ): bool {
		$chain = $this->getRedirectChain();
		$key   = $this->normalizePathKey( $path );
		$count = 0;
		foreach ( $chain as $entry ) {
			if ( $entry === $key ) {
				++$count;
			}
		}

		return $count >= 2;
	}

	private function isRedirectChainLoop( string $source_path, string $target_path ): bool {
		$source_key = $this->normalizePathKey( $source_path );
		$target_key = $this->normalizePathKey( $target_path );

		if ( $source_key === $target_key ) {
			return true;
		}

		$chain = $this->getRedirectChain();
		if ( in_array( $target_key, $chain, true ) ) {
			return true;
		}

		return count( $chain ) >= self::LOOP_MAX_CHAIN;
	}

	private function recordRedirectChain( string $source_path, string $target_path ): void {
		$chain   = $this->getRedirectChain();
		$chain[] = $this->normalizePathKey( $source_path );
		$chain[] = $this->normalizePathKey( $target_path );
		$chain   = array_values( array_slice( $chain, -self::LOOP_MAX_CHAIN ) );
		$json    = wp_json_encode( $chain );

		if ( $json !== false && ! headers_sent() ) {
			setcookie( self::LOOP_CHAIN_COOKIE, $json, time() + 300, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
		}
	}

	/**
	 * @return list<string>
	 */
	private function getRedirectChain(): array {
		$raw = isset( $_COOKIE[ self::LOOP_CHAIN_COOKIE ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::LOOP_CHAIN_COOKIE ] ) ) : '';
		if ( $raw === '' ) {
			return [];
		}

		$decoded = json_decode( $raw, true );
		if ( ! is_array( $decoded ) ) {
			return [];
		}

		return array_values( array_filter( array_map( [ $this, 'normalizePathKey' ], $decoded ) ) );
	}

	private function normalizePathKey( string $path ): string {
		$path = wp_parse_url( $path, PHP_URL_PATH );
		if ( ! is_string( $path ) || $path === '' ) {
			$path = '/';
		}

		return rtrim( $path, '/' ) ?: '/';
	}

	/**
	 * Detect whether saving a rule would create a redirect loop (H5).
	 */
	private function detectPotentialRedirectLoop( string $source, string $target, string $match_type, ?string $exclude_id = null ): ?string {
		if ( $match_type === 'regex' ) {
			return null;
		}

		$source_key = $this->normalizePathKey( $this->normalizeSourceToPath( $source ) );
		$target_key = $this->normalizePathKey( $this->normalizeSourceToPath( $target ) );

		if ( $source_key === $target_key ) {
			return 'redirect_loop';
		}

		$rules = array_filter(
			$this->getAllRedirects(),
			static function ( array $rule ) use ( $exclude_id ): bool {
				if ( empty( $rule['active'] ) ) {
					return false;
				}
				return ( $rule['id'] ?? '' ) !== $exclude_id;
			}
		);

		$visited = [ $source_key ];
		$current = $target_key;
		$depth   = 0;

		while ( $depth < 12 ) {
			if ( in_array( $current, $visited, true ) ) {
				return 'redirect_loop';
			}
			$visited[] = $current;

			$next_path = null;
			foreach ( $rules as $rule ) {
				$match = $this->matchRule( $current, $rule );
				if ( $match === false ) {
					continue;
				}
				$next_url  = $this->resolveTargetUrl( $rule['target'], $match );
				$parsed    = wp_parse_url( $next_url, PHP_URL_PATH );
				$next_path = is_string( $parsed ) && $parsed !== '' ? $parsed : '/';
				break;
			}

			if ( $next_path === null ) {
				break;
			}

			$current = $this->normalizePathKey( $next_path );
			++$depth;
		}

		return null;
	}

	private function queueHitCount( string $id ): void {
		if ( $id === '' ) {
			return;
		}

		$this->pending_hit_counts[ $id ] = ( $this->pending_hit_counts[ $id ] ?? 0 ) + 1;

		if ( ! $this->hit_flush_registered ) {
			$this->hit_flush_registered = true;
			add_action( 'shutdown', [ $this, 'flushHitCounts' ], 0 );
		}
	}

	/**
	 * Flush queued redirect hit counts after the response is sent (H4).
	 */
	public function flushHitCounts(): void {
		if ( $this->pending_hit_counts === [] ) {
			return;
		}

		global $wpdb;
		$table = $this->getTableName();
		$now   = gmdate( 'Y-m-d H:i:s' );

		foreach ( $this->pending_hit_counts as $id => $count ) {
			$wpdb->query(
				$wpdb->prepare(
					"UPDATE {$table} SET hit_count = hit_count + %d, last_hit = %s WHERE id = %s",
					$count,
					$now,
					$id
				)
			);
		}

		$this->pending_hit_counts = [];
	}

	private function getHttpCode( string $type ): int {
		$map = [
			'301' => 301, '302' => 302, '303' => 303, '307' => 307, '308' => 308,
			'401' => 401, '402' => 402, '403' => 403, '404' => 404, '405' => 405,
			'410' => 410, '501' => 501, '502' => 502, '503' => 503,
		];
		return $map[ $type ] ?? 301;
	}

	private function getDefaultErrorMessage( string $type ): string {
		$defaults = [
			'401' => __( 'You do not have permission to access this page.', 'wp-powersuite' ),
			'402' => __( 'Payment required.', 'wp-powersuite' ),
			'403' => __( 'You do not have permission to access this page.', 'wp-powersuite' ),
			'404' => __( 'Page not found.', 'wp-powersuite' ),
			'405' => __( 'Method not allowed.', 'wp-powersuite' ),
			'410' => __( 'This resource is no longer available.', 'wp-powersuite' ),
			'501' => __( 'Not implemented.', 'wp-powersuite' ),
			'502' => __( 'Bad gateway.', 'wp-powersuite' ),
			'503' => __( 'Service temporarily unavailable.', 'wp-powersuite' ),
		];
		return $defaults[ $type ] ?? __( 'Page not found.', 'wp-powersuite' );
	}

	private function getActiveRules(): array {
		$cached = get_transient( self::CACHE_KEY );
		if ( $cached !== false && is_array( $cached ) ) {
			return $cached;
		}

		global $wpdb;
		$rows = $wpdb->get_results( $wpdb->prepare(
			"SELECT * FROM {$this->getTableName()} WHERE is_active = %d ORDER BY priority ASC, LENGTH(source_url) DESC",
			1
		) );
		$active = [];
		foreach ( (array) $rows as $row ) {
			$active[] = $this->dbToRow( $row );
		}

		set_transient( self::CACHE_KEY, $active, self::CACHE_TTL );
		return $active;
	}

	private function incrementHitCount( string $id ): void {
		$this->queueHitCount( $id );
	}

	public function clearCache(): void {
		delete_transient( self::CACHE_KEY );
	}

	/**
	 * Add a 301 redirect rule programmatically (e.g. from Media Rename Manager).
	 * Accepts full URLs or paths; extracts path for source/target.
	 *
	 * @param string $old_url Old URL or path (e.g. https://example.com/wp-content/uploads/old.jpg or /wp-content/uploads/old.jpg).
	 * @param string $new_url New URL or path.
	 * @return bool True if added, false if Redirect Manager disabled or invalid URLs.
	 */
	public static function addRedirectRule( string $old_url, string $new_url ): bool {
		if ( ! OptionStoreModuleSettingsRepository::isModuleEnabled( 'redirect-manager' ) ) {
			return false;
		}
		$old_parsed = parse_url( $old_url );
		$new_parsed = parse_url( $new_url );
		$source = isset( $old_parsed['path'] ) ? $old_parsed['path'] : $old_url;
		$target = isset( $new_parsed['path'] ) ? $new_parsed['path'] : $new_url;
		$source = '/' . ltrim( $source, '/' );
		$target = '/' . ltrim( $target, '/' );
		if ( $source === '/' || $target === '/' || $source === $target ) {
			return false;
		}
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;
		$exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE source_url = %s", $source ) );
		if ( $exists ) {
			$wpdb->update(
				$table,
				[
					'target_url' => $target,
					'is_active'  => 1,
				],
				[ 'id' => $exists ],
				[ '%s', '%d' ],
				[ '%s' ]
			);
			delete_transient( self::CACHE_KEY );
			return true;
		}
		$wpdb->insert(
			$table,
			[
				'id'             => wp_generate_uuid4(),
				'source_url'     => $source,
				'target_url'     => $target,
				'redirect_type'  => '301',
				'match_type'     => 'exact',
				'is_active'      => 1,
				'priority'       => 10,
				'notes'          => '',
				'strip_params'   => 0,
				'custom_message' => '',
				'conditions_json'=> '[]',
				'hit_count'      => 0,
				'last_hit'       => null,
			],
			[ '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%s', '%s', '%d', '%s' ]
		);
		delete_transient( self::CACHE_KEY );
		return true;
	}

	public function handleSave(): void {
		if ( ! isset( $_POST['wpps_redirect_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpps_redirect_nonce'] ) ), 'wpps_redirect_save' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}

		$source = isset( $_POST['source'] ) ? sanitize_text_field( wp_unslash( $_POST['source'] ) ) : '';
		$target_raw = isset( $_POST['target'] ) ? wp_unslash( $_POST['target'] ) : '';
		$target = preg_match( '#^https?://#i', $target_raw ) ? esc_url_raw( $target_raw ) : sanitize_text_field( $target_raw );
		$redirect_types = [ '301', '302', '303', '307', '308' ];
		$error_types = [ '401', '402', '403', '404', '405', '410', '501', '502', '503' ];
		$allowed_types = array_merge( $redirect_types, $error_types );
		$type = isset( $_POST['type'] ) && in_array( $_POST['type'], $allowed_types, true ) ? $_POST['type'] : '301';
		$match = isset( $_POST['match_type'] ) && in_array( $_POST['match_type'], [ 'exact', 'wildcard', 'regex' ], true ) ? $_POST['match_type'] : 'exact';
		$id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
		$priority = isset( $_POST['priority'] ) ? max( 0, (int) $_POST['priority'] ) : 10;
		$notes = isset( $_POST['notes'] ) ? sanitize_textarea_field( wp_unslash( $_POST['notes'] ) ) : '';
		$strip_params = ! empty( $_POST['strip_params'] );
		$message = isset( $_POST['message'] ) ? sanitize_text_field( wp_unslash( $_POST['message'] ) ) : '';

		$conditions = [];
		if ( ! empty( $_POST['cond_login_enabled'] ) ) {
			$conditions['login_status'] = [
				'enabled' => true,
				'status' => isset( $_POST['cond_login_status'] ) && $_POST['cond_login_status'] === 'logged_in' ? 'logged_in' : 'logged_out',
			];
		}
		if ( ! empty( $_POST['cond_referrer_enabled'] ) ) {
			$conditions['referrer'] = [
				'enabled' => true,
				'pattern' => isset( $_POST['cond_referrer_pattern'] ) ? sanitize_text_field( wp_unslash( $_POST['cond_referrer_pattern'] ) ) : '',
			];
		}
		if ( ! empty( $_POST['cond_param_enabled'] ) ) {
			$conditions['url_param'] = [
				'enabled' => true,
				'name' => isset( $_POST['cond_param_name'] ) ? sanitize_text_field( wp_unslash( $_POST['cond_param_name'] ) ) : '',
				'value' => isset( $_POST['cond_param_value'] ) ? sanitize_text_field( wp_unslash( $_POST['cond_param_value'] ) ) : '',
			];
		}
		if ( ! empty( $_POST['cond_language_enabled'] ) ) {
			$conditions['language'] = [
				'enabled' => true,
				'codes' => isset( $_POST['cond_language_codes'] ) ? sanitize_text_field( wp_unslash( $_POST['cond_language_codes'] ) ) : '',
			];
		}

		$target_required = ! in_array( $type, $error_types, true );
		if ( $source === '' ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'missing_source' ], admin_url( 'admin.php' ) ) );
			exit;
		}
		if ( $target_required && $target === '' ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'missing_target' ], admin_url( 'admin.php' ) ) );
			exit;
		}

		if ( $match === 'regex' && ! RegexGuard::isSafePattern( $source ) ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'invalid_regex' ], admin_url( 'admin.php' ) ) );
			exit;
		}

		if ( $match !== 'regex' ) {
			$source = $this->normalizeSourceToPath( $source );
		}

		// Block duplicate source + match_type (causes ambiguous redirects)
		$dup = $this->findDuplicateSource( $source, $id ?: null, $match );
		if ( $dup !== null ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'duplicate_source' ], admin_url( 'admin.php' ) ) );
			exit;
		}

		if ( $target_required && $this->detectPotentialRedirectLoop( $source, $target, $match, $id ?: null ) !== null ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'redirect_loop' ], admin_url( 'admin.php' ) ) );
			exit;
		}

		global $wpdb;
		$this->ensureTableExists();

		$row_id = $id ?: wp_generate_uuid4();
		$active = 1;
		if ( $id ) {
			$existing = $wpdb->get_row( $wpdb->prepare( "SELECT is_active, hit_count, last_hit FROM {$this->getTableName()} WHERE id = %s", $id ) );
			if ( $existing ) {
				$active = (int) $existing->is_active;
			}
		}

		$db_row = [
			'id'             => $row_id,
			'source_url'     => $source,
			'target_url'     => $target,
			'redirect_type'  => $type,
			'match_type'     => $match,
			'is_active'      => $active,
			'priority'       => $priority,
			'notes'          => $notes,
			'strip_params'   => $strip_params ? 1 : 0,
			'custom_message' => $message,
			'conditions_json'=> wp_json_encode( $conditions ),
		];

		if ( $id ) {
			$wpdb->update(
				$this->getTableName(),
				$db_row,
				[ 'id' => $id ],
				[ '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%d', '%s', '%s' ],
				[ '%s' ]
			);
		} else {
			$db_row['hit_count'] = 0;
			$db_row['last_hit']  = null;
			$this->insertRedirect( $db_row );
		}

		$this->clearCache();

		wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'saved' => '1' ], admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handleDelete(): void {
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpps_redirect_delete' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}
		$id = isset( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : '';
		if ( $id === '' ) {
			wp_safe_redirect( add_query_arg( 'page', 'powersuite-redirect-manager', admin_url( 'admin.php' ) ) );
			exit;
		}
		global $wpdb;
		$wpdb->delete( $this->getTableName(), [ 'id' => $id ], [ '%s' ] );
		$this->clearCache();
		wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'deleted' => '1' ], admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handleBulk(): void {
		if ( ! isset( $_POST['wpps_bulk_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpps_bulk_nonce'] ) ), 'wpps_redirect_bulk' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}
		$action = isset( $_POST['bulk_action'] ) ? sanitize_text_field( wp_unslash( $_POST['bulk_action'] ) ) : '';
		$ids = isset( $_POST['ids'] ) && is_array( $_POST['ids'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['ids'] ) ) : [];
		$ids = array_filter( $ids );
		if ( empty( $ids ) || $action === '' ) {
			wp_safe_redirect( add_query_arg( 'page', 'powersuite-redirect-manager', admin_url( 'admin.php' ) ) );
			exit;
		}

		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%s' ) );
		global $wpdb;
		if ( $action === 'delete' ) {
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$this->getTableName()} WHERE id IN ($placeholders)", $ids ) );
		} elseif ( $action === 'enable' ) {
			$wpdb->query( $wpdb->prepare( "UPDATE {$this->getTableName()} SET is_active = 1 WHERE id IN ($placeholders)", $ids ) );
		} elseif ( $action === 'disable' ) {
			$wpdb->query( $wpdb->prepare( "UPDATE {$this->getTableName()} SET is_active = 0 WHERE id IN ($placeholders)", $ids ) );
		}

		$this->clearCache();
		wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'bulk' => '1' ], admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handleClearCache(): void {
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpps_redirect_clear_cache' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}
		$this->clearCache();
		wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'cache_cleared' => '1' ], admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handleDuplicate(): void {
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpps_redirect_duplicate' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}
		$id = isset( $_GET['id'] ) ? sanitize_text_field( wp_unslash( $_GET['id'] ) ) : '';
		if ( $id === '' ) {
			wp_safe_redirect( add_query_arg( 'page', 'powersuite-redirect-manager', admin_url( 'admin.php' ) ) );
			exit;
		}
		$new = $this->duplicateRedirect( $id );
		$msg = $new ? 'duplicated' : 'duplicate_failed';
		wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'message' => $msg ], admin_url( 'admin.php' ) ) );
		exit;
	}

	public function handleImport(): void {
		if ( ! isset( $_POST['wpps_import_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpps_import_nonce'] ) ), 'wpps_redirect_import' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}
		$tmp = isset( $_FILES['import_file']['tmp_name'] ) ? $_FILES['import_file']['tmp_name'] : '';
		if ( empty( $tmp ) || ! is_uploaded_file( $tmp ) ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'import_empty' ], admin_url( 'admin.php' ) ) );
			exit;
		}
		$file_size = isset( $_FILES['import_file']['size'] ) ? (int) $_FILES['import_file']['size'] : 0;
		if ( $file_size <= 0 || $file_size > self::IMPORT_MAX_BYTES ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'import_too_large' ], admin_url( 'admin.php' ) ) );
			exit;
		}
		$csv = file_get_contents( $tmp );
		if ( $csv === false || $csv === '' ) {
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => 'import_empty' ], admin_url( 'admin.php' ) ) );
			exit;
		}

		$result = $this->importRedirectsFromCsvString( (string) $csv );

		if ( ! empty( $result['error'] ) ) {
			$error_arg = $result['error'] === 'too_many' ? 'import_too_many' : 'import_empty';
			wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'error' => $error_arg ], admin_url( 'admin.php' ) ) );
			exit;
		}

		wp_safe_redirect( add_query_arg( [ 'page' => 'powersuite-redirect-manager', 'imported' => (string) ( $result['imported'] ?? 0 ) ], admin_url( 'admin.php' ) ) );
		exit;
	}

	/**
	 * Import redirects from a CSV string.
	 *
	 * Shared engine for the admin upload handler and the WP-CLI importer, so
	 * validation, sanitization, and duplicate handling stay identical across
	 * both entry points. Never emits output, redirects, or exits.
	 *
	 * @since 1.0.7
	 * @param string $csv     Raw CSV content (header row optional).
	 * @param bool   $dry_run When true, validate and count without writing rows.
	 * @return array{imported:int, skipped:int, error?:string}
	 */
	public function importRedirectsFromCsvString( string $csv, bool $dry_run = false ): array {
		if ( trim( $csv ) === '' ) {
			return [ 'imported' => 0, 'skipped' => 0, 'error' => 'empty' ];
		}

		$lines = array_filter( array_map( 'trim', explode( "\n", $csv ) ) );
		if ( count( $lines ) > self::IMPORT_MAX_ROWS + 1 ) {
			return [ 'imported' => 0, 'skipped' => 0, 'error' => 'too_many' ];
		}

		$imported          = 0;
		$skipped           = 0;
		$valid_match_types = [ 'exact', 'wildcard', 'regex' ];
		$seen_in_batch     = [];

		foreach ( $lines as $i => $line ) {
			if ( $i === 0 && stripos( $line, 'source' ) !== false ) {
				continue;
			}
			$cols = str_getcsv( $line );
			if ( count( $cols ) < 2 ) {
				++$skipped;
				continue;
			}
			$source = sanitize_text_field( $cols[0] );
			$target = sanitize_text_field( $cols[1] );
			if ( strlen( $source ) > self::IMPORT_MAX_FIELD_LENGTH || strlen( $target ) > self::IMPORT_MAX_FIELD_LENGTH ) {
				++$skipped;
				continue;
			}
			$type       = isset( $cols[2] ) ? sanitize_text_field( $cols[2] ) : '301';
			$match_type = isset( $cols[3] ) ? sanitize_key( $cols[3] ) : 'exact';
			if ( ! in_array( $match_type, $valid_match_types, true ) ) {
				$match_type = 'exact';
			}
			$valid_types = [ '301', '302', '303', '307', '308', '401', '402', '403', '404', '405', '410', '501', '502', '503' ];
			if ( ! in_array( $type, $valid_types, true ) ) {
				$type = '301';
			}
			$error_types     = [ '401', '402', '403', '404', '405', '410', '501', '502', '503' ];
			$target_required = ! in_array( $type, $error_types, true );
			if ( $match_type !== 'regex' ) {
				$source = $this->normalizeSourceToPath( $source );
			} elseif ( ! RegexGuard::isSafePattern( $source ) ) {
				++$skipped;
				continue;
			}
			if ( $source === '/' || ( $target_required && $target === '' ) ) {
				++$skipped;
				continue;
			}

			$dedupe_key = $match_type . '|' . $source;
			if ( isset( $seen_in_batch[ $dedupe_key ] ) || $this->findDuplicateSource( $source, null, $match_type ) !== null ) {
				++$skipped;
				continue;
			}
			$seen_in_batch[ $dedupe_key ] = true;

			if ( ! $dry_run ) {
				$row = [
					'id'           => wp_generate_uuid4(),
					'source'       => $source,
					'target'       => $target,
					'type'         => $type,
					'match_type'   => $match_type,
					'active'       => true,
					'priority'     => 10,
					'notes'        => isset( $cols[4] ) ? sanitize_text_field( $cols[4] ) : '',
					'strip_params' => false,
					'message'      => '',
					'conditions'   => [],
					'hits'         => 0,
					'last_hit'     => null,
				];
				$this->insertRedirect( $this->rowToDb( $row ) );
			}
			++$imported;
		}

		if ( ! $dry_run && $imported > 0 ) {
			$this->clearCache();
		}

		return [ 'imported' => $imported, 'skipped' => $skipped ];
	}

	public function handleExport(): void {
		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'wpps_redirect_export' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="redirects-' . gmdate( 'Y-m-d' ) . '.csv"' );
		echo $this->exportRedirectsToCsvString(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download payload.
		exit;
	}

	/**
	 * Build the CSV representation of all redirects.
	 *
	 * Shared by the admin export handler and the WP-CLI exporter.
	 *
	 * @since 1.0.7
	 * @return string CSV content including the header row.
	 */
	public function exportRedirectsToCsvString(): string {
		$redirects = $this->getAllRedirects();
		$out       = "source,target,type,match_type,notes\n";
		foreach ( $redirects as $r ) {
			$out .= sprintf(
				'"%s","%s","%s","%s","%s"' . "\n",
				str_replace( '"', '""', $r['source'] ?? '' ),
				str_replace( '"', '""', $r['target'] ?? '' ),
				$r['type'] ?? '301',
				$r['match_type'] ?? 'exact',
				str_replace( '"', '""', $r['notes'] ?? '' )
			);
		}
		return $out;
	}

	public function ajaxToggle(): void {
		check_ajax_referer( 'wpps_redirect_manager', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
		if ( $id === '' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid ID.', 'wp-powersuite' ) ] );
		}
		global $wpdb;
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT is_active FROM {$this->getTableName()} WHERE id = %s", $id ) );
		if ( ! $row ) {
			wp_send_json_error( [ 'message' => __( 'Redirect not found.', 'wp-powersuite' ) ] );
		}
		$new_active = empty( $row->is_active ) ? 1 : 0;
		$wpdb->update( $this->getTableName(), [ 'is_active' => $new_active ], [ 'id' => $id ], [ '%d' ], [ '%s' ] );
		$this->clearCache();
		wp_send_json_success( [ 'active' => (bool) $new_active ] );
	}

	public function ajaxGetPages(): void {
		check_ajax_referer( 'wpps_redirect_manager', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [] );
		}
		$pages = get_pages( [ 'number' => 200, 'post_status' => 'publish' ] );
		$posts = get_posts( [ 'numberposts' => 100, 'post_status' => 'publish', 'post_type' => 'post' ] );
		$items = [];
		foreach ( array_merge( $pages, $posts ) as $p ) {
			$items[] = [ 'id' => $p->ID, 'url' => get_permalink( $p ), 'title' => $p->post_title ];
		}
		wp_send_json_success( $items );
	}

	public function ajaxTestUrl(): void {
		check_ajax_referer( 'wpps_redirect_manager', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}
		$raw = isset( $_POST['url'] ) ? sanitize_text_field( wp_unslash( $_POST['url'] ) ) : '';
		if ( $raw === '' ) {
			wp_send_json_error( [ 'message' => __( 'Enter a URL to test.', 'wp-powersuite' ) ] );
		}
		$url = esc_url_raw( $raw );
		if ( $url === '' && str_starts_with( $raw, '/' ) ) {
			$url = home_url( $raw );
		}
		if ( $url === '' ) {
			wp_send_json_error( [ 'message' => __( 'Enter a valid URL or path.', 'wp-powersuite' ) ] );
		}
		$path  = RequestPathHelper::stripHomePath( RequestPathHelper::pathFromRequestUri( $url ) ?: '/' );
		$path  = rtrim( $path, '/' ) ?: '/';
		$query = RequestPathHelper::queryFromRequestUri( str_starts_with( $raw, '/' ) ? $raw : $url );
		$get   = [];
		if ( $query !== '' ) {
			parse_str( $query, $get );
		}

		$rules           = $this->getActiveRules();
		$result          = null;
		$path_match_only = null;
		$test_overrides  = [
			'skip_login' => true,
			'get'        => $get,
		];

		foreach ( $rules as $rule ) {
			$match = $this->matchRule( $path, $rule );
			if ( $match === false ) {
				continue;
			}

			if ( ! $this->checkConditions( $rule, $test_overrides ) ) {
				if ( $path_match_only === null ) {
					$path_match_only = $rule['source'] ?? '';
				}
				continue;
			}

			$target = $this->resolveTargetUrl( $rule['target'], $match );
			$code   = (int) ( $rule['type'] ?? 301 );
			$result = [
				'match'                   => true,
				'source'                  => $rule['source'],
				'target'                  => $target,
				'type'                    => $rule['type'] ?? '301',
				'name'                    => $rule['notes'] ?: $rule['source'],
				'is_error'                => $code >= 400,
				'login_condition_ignored' => ! empty( $rule['conditions']['login_status']['enabled'] ),
			];
			break;
		}
		if ( $result ) {
			wp_send_json_success( $result );
		}
		if ( $path_match_only !== null ) {
			wp_send_json_success(
				[
					'match'   => false,
					'message' => sprintf(
						/* translators: %s: source URL pattern */
						__( 'Path matched “%s”, but other conditions (referrer, URL param, or language) did not pass. Login status is ignored in Test URL.', 'wp-powersuite' ),
						$path_match_only
					),
				]
			);
		}
		wp_send_json_success( [ 'match' => false, 'message' => __( 'No matching redirect found.', 'wp-powersuite' ) ] );
	}

	public function duplicateRedirect( string $id ): ?array {
		$redirects = $this->getAllRedirects();
		foreach ( $redirects as $r ) {
			if ( ( $r['id'] ?? '' ) === $id ) {
				$new = $r;
				unset( $new['hits'], $new['last_hit'] );
				$new['id'] = wp_generate_uuid4();
				$new['active'] = false;
				$src = rtrim( $new['source'] ?? '', '/' );
				$new['source'] = $src . '-copy';
				$this->insertRedirect( $this->rowToDb( $new ) );
				$this->clearCache();
				return $new;
			}
		}
		return null;
	}

	public function getAllRedirects(): array {
		global $wpdb;
		$this->ensureTableExists();
		$table = $this->getTableName();
		$rows  = $wpdb->get_results( "SELECT * FROM {$table} ORDER BY priority ASC, source_url ASC" );
		$list = [];
		foreach ( (array) $rows as $row ) {
			$list[] = $this->dbToRow( $row );
		}
		return $list;
	}

	public function findDuplicateSource( string $source, ?string $exclude_id = null, ?string $match_type = null ): ?array {
		$redirects = $this->getAllRedirects();
		$norm      = ( $match_type === 'regex' ) ? $source : $this->normalizeSourceToPath( $source );
		foreach ( $redirects as $r ) {
			if ( ( $r['id'] ?? '' ) === $exclude_id ) {
				continue;
			}
			$existing_type = $r['match_type'] ?? 'exact';
			if ( $match_type !== null && $existing_type !== $match_type ) {
				continue;
			}
			$existing = (string) ( $r['source'] ?? '' );
			if ( $existing_type !== 'regex' ) {
				$existing = $this->normalizeSourceToPath( $existing );
			}
			if ( $existing !== $norm ) {
				continue;
			}
			return $r;
		}
		return null;
	}


}
