<?php
/** Shared Rank Math redirection state, validation, locking and verification. */

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

define( 'WPAGENT_RANK_MATH_REDIRECTION_LOCK_OPTION', 'wp_agent_rank_math_redirection_lock' );
define( 'WPAGENT_RANK_MATH_REDIRECTION_LOCK_TTL', 60 );
define( 'WPAGENT_RANK_MATH_REDIRECTION_MAX_RECORDS', 5000 );

function wpagent_perm_rank_math_redirections( $input = null ): bool {
	unset( $input );
	return current_user_can( 'rank_math_redirections' );
}

/** @return true|WP_Error */
function wpagent_rank_math_redirection_runtime() {
	if ( ! class_exists( 'RankMath\\Helper' ) || ! class_exists( 'RankMath\\Redirections\\DB' ) || ! class_exists( 'RankMath\\Redirections\\Redirection' ) ) {
		return new WP_Error( 'wpagent_seo_redirections_unavailable', __( 'Rank Math mit Weiterleitungsmodul ist nicht verfügbar.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( ! \RankMath\Helper::is_module_active( 'redirections', false ) ) {
		return new WP_Error( 'wpagent_seo_redirections_disabled', __( 'Das Rank-Math-Weiterleitungsmodul ist nicht aktiviert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	if ( ! class_exists( 'RankMath\\Helpers\\DB' )
		|| ! \RankMath\Helpers\DB::check_table_exists( 'rank_math_redirections' )
		|| ! \RankMath\Helpers\DB::check_table_exists( 'rank_math_redirections_cache' ) ) {
		return new WP_Error( 'wpagent_seo_redirections_storage_missing', __( 'Die Rank-Math-Weiterleitungstabellen fehlen.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return true;
}

/**
 * Rank Math uses MySQL's unary BINARY operator for case-sensitive cache
 * lookups. SQLite compares text with its BINARY collation by default, while
 * the official WordPress SQLite driver cannot parse that unary operator.
 * Rewrite only the affected Rank Math SELECT and leave all other SQL intact.
 */
function wpagent_rank_math_redirection_sqlite_query_compat( string $query ): string {
	global $wpdb;
	if ( ! $wpdb instanceof WP_SQLite_DB
		|| ! preg_match( '/^\s*SELECT\b/i', $query )
		|| ! preg_match(
			'/\bFROM\s+`?' . preg_quote( $wpdb->prefix . 'rank_math_redirections_cache', '/' ) . '`?(?:\s|$)/i',
			$query
		) ) {
		return $query;
	}
	$rewritten = preg_replace( '/\bBINARY\s+(`?from_url`?)(?=\s*=)/i', '$1', $query );
	return is_string( $rewritten ) ? $rewritten : $query;
}

if ( isset( $GLOBALS['wpdb'] ) && $GLOBALS['wpdb'] instanceof WP_SQLite_DB ) {
	add_filter( 'query', 'wpagent_rank_math_redirection_sqlite_query_compat', 10 );
}

/** @return string|null */
function wpagent_rank_math_redirection_canonical_path( string $path ): ?string {
	if ( '' === $path || '/' !== $path[0] || strlen( $path ) > 4096 || ! wp_is_valid_utf8( $path )
		|| preg_match( '/[\x00-\x1F\x7F\\\\]/u', $path ) || preg_match( '/%(?:00|0a|0d|2f|5c)/i', $path ) ) {
		return null;
	}
	$decoded = rawurldecode( $path );
	if ( ! wp_is_valid_utf8( $decoded ) || preg_match( '/[\x00-\x1F\x7F\\\\]/u', $decoded )
		|| false !== strpbrk( $decoded, '?#' ) || false !== strpos( $decoded, '%' ) ) {
		return null;
	}
	foreach ( explode( '/', $decoded ) as $segment ) {
		if ( '.' === $segment || '..' === $segment ) {
			return null;
		}
	}
	return '/' === $decoded ? '/' : untrailingslashit( $decoded );
}

/** @param array<string,mixed> $parts */
function wpagent_rank_math_redirection_normalized_port( array $parts ): int {
	$port   = absint( $parts['port'] ?? 0 );
	$scheme = strtolower( (string) ( $parts['scheme'] ?? '' ) );
	return ( 80 === $port && 'http' === $scheme ) || ( 443 === $port && 'https' === $scheme ) ? 0 : $port;
}

/** @param array<string,mixed> $left @param array<string,mixed> $right */
function wpagent_rank_math_redirection_same_site_endpoint( array $left, array $right ): bool {
	if ( ! isset( $left['host'], $right['host'] ) ) {
		return false;
	}
	$left_host  = strtolower( rtrim( (string) $left['host'], '.' ) );
	$right_host = strtolower( rtrim( (string) $right['host'], '.' ) );
	return '' !== $left_host
		&& $left_host === $right_host
		&& wpagent_rank_math_redirection_normalized_port( $left ) === wpagent_rank_math_redirection_normalized_port( $right );
}

/** @return string|WP_Error */
function wpagent_rank_math_redirection_validate_source( $value ) {
	if ( ! is_string( $value ) || '' === $value || strlen( $value ) > 2048 || '/' !== $value[0] || str_starts_with( $value, '//' ) ) {
		return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'source muss ein lokaler, nicht leerer Pfad mit führendem Schrägstrich sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! wp_is_valid_utf8( $value ) || preg_match( '/[\x00-\x1F\x7F\\\\]/u', $value ) || preg_match( '/%(?:00|0a|0d|2f|5c)/i', $value ) ) {
		return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'source enthält eine mehrdeutige oder nicht erlaubte Kodierung.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$parts = wp_parse_url( $value );
	if ( false === $parts || isset( $parts['scheme'], $parts['host'] ) || isset( $parts['query'] ) || isset( $parts['fragment'] ) || ! isset( $parts['path'] ) ) {
		return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'source darf nur einen lokalen Pfad ohne Query oder Fragment enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$decoded = rawurldecode( $parts['path'] );
	if ( ! wp_is_valid_utf8( $decoded ) || preg_match( '/[\x00-\x1F\x7F\\\\]/u', $decoded ) || false !== strpbrk( $decoded, '?#' ) ) {
		return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'source enthält nach der Dekodierung nicht erlaubte URL-Zeichen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( false !== strpos( $decoded, '%' ) ) {
		return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'source darf keine mehrdeutig verschachtelte Prozentkodierung enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	foreach ( explode( '/', $decoded ) as $segment ) {
		if ( '.' === $segment || '..' === $segment ) {
			return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'source darf keine Punktsegmente enthalten.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
	}
	if ( '/' === $decoded ) {
		return new WP_Error( 'wpagent_seo_redirection_source_invalid', __( 'Die Startseite darf nicht als Weiterleitungsquelle verwendet werden.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return untrailingslashit( $decoded );
}

/** @return string|WP_Error */
function wpagent_rank_math_redirection_validate_destination( $value, int $http_code ) {
	if ( in_array( $http_code, array( 410, 451 ), true ) ) {
		return '' === $value
			? ''
			: new WP_Error( 'wpagent_seo_redirection_terminal_destination', __( 'HTTP 410 und 451 dürfen kein Weiterleitungsziel besitzen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	if ( ! is_string( $value ) || '' === $value || strlen( $value ) > 4096 || ! wp_is_valid_utf8( $value ) || preg_match( '/[\x00-\x1F\x7F]/u', $value ) ) {
		return new WP_Error( 'wpagent_seo_redirection_destination_invalid', __( 'destination muss eine gültige HTTP- oder HTTPS-URL sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$absolute = str_starts_with( $value, '/' ) && ! str_starts_with( $value, '//' ) ? home_url( $value ) : $value;
	$parts    = wp_parse_url( $absolute );
	if ( false === $parts || ! isset( $parts['scheme'], $parts['host'] )
		|| ! in_array( strtolower( (string) $parts['scheme'] ), array( 'http', 'https' ), true )
		|| isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
		return new WP_Error( 'wpagent_seo_redirection_destination_invalid', __( 'destination muss eine HTTP- oder HTTPS-URL ohne Zugangsdaten sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$sanitized = esc_url_raw( $absolute, array( 'http', 'https' ) );
	if ( '' === $sanitized || $sanitized !== $absolute ) {
		return new WP_Error( 'wpagent_seo_redirection_destination_invalid', __( 'destination wurde wegen ungültiger URL-Bestandteile abgelehnt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$home = wp_parse_url( home_url( '/' ) );
	if ( false !== $home && wpagent_rank_math_redirection_same_site_endpoint( $home, $parts )
		&& null === wpagent_rank_math_redirection_canonical_path( (string) ( $parts['path'] ?? '/' ) ) ) {
		return new WP_Error( 'wpagent_seo_redirection_destination_invalid', __( 'destination wurde wegen ungültiger URL-Bestandteile abgelehnt.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	return $absolute;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_rank_math_redirection_payload( array $record, bool $filter_readback = true ) {
	$id      = absint( $record['id'] ?? 0 );
	$sources = $record['sources'] ?? null;
	if ( $id <= 0 || ! is_array( $sources ) || array() === $sources || count( $sources ) > 20 ) {
		return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte einen ungültigen Weiterleitungszustand.', 'wp-agent-companion' ), array( 'status' => 502 ) );
	}
	$normalized_sources = array();
	foreach ( $sources as $source ) {
		if ( ! is_array( $source ) || ! is_string( $source['pattern'] ?? null ) || '' === $source['pattern'] ) {
			return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte eine ungültige Weiterleitungsquelle.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$comparison = is_string( $source['comparison'] ?? null ) ? $source['comparison'] : 'exact';
		$ignore     = is_string( $source['ignore'] ?? null ) ? $source['ignore'] : '';
		if ( ! in_array( $comparison, array( 'exact', 'contains', 'start', 'end', 'regex' ), true ) || ! in_array( $ignore, array( '', 'case' ), true ) ) {
			return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte einen unbekannten Quellenvergleich.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$pattern = '/' . ltrim( $source['pattern'], '/' );
		if ( strlen( $pattern ) > 2048 || ! wp_is_valid_utf8( $pattern ) || preg_match( '/[\x00-\x1F\x7F]/u', $pattern ) ) {
			return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte einen nicht sicheren Quellenpfad.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		if ( 'exact' === $comparison ) {
			$canonical_pattern = wpagent_rank_math_redirection_validate_source( $pattern );
			if ( ! is_wp_error( $canonical_pattern ) ) {
				$pattern = $canonical_pattern;
			}
		}
		$normalized_sources[] = array( 'pattern' => $pattern, 'comparison' => $comparison, 'ignore_case' => 'case' === $ignore );
	}
	$http_code = absint( $record['header_code'] ?? 0 );
	if ( ! in_array( $http_code, array( 301, 302, 307, 410, 451 ), true ) ) {
		return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte einen nicht unterstützten HTTP-Status.', 'wp-agent-companion' ), array( 'status' => 502 ) );
	}
	$destination = wpagent_rank_math_redirection_validate_destination( (string) ( $record['url_to'] ?? '' ), $http_code );
	if ( is_wp_error( $destination ) ) {
		return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte ein ungültiges Weiterleitungsziel.', 'wp-agent-companion' ), array( 'status' => 502 ) );
	}
	$status = is_string( $record['status'] ?? null ) ? $record['status'] : '';
	if ( ! in_array( $status, array( 'active', 'inactive', 'trashed' ), true ) ) {
		return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte einen unbekannten Weiterleitungsstatus.', 'wp-agent-companion' ), array( 'status' => 502 ) );
	}
	$writable = 1 === count( $normalized_sources ) && 'exact' === $normalized_sources[0]['comparison']
		&& ! is_wp_error( wpagent_rank_math_redirection_validate_source( $normalized_sources[0]['pattern'] ) );
	$state = array(
		'redirection_id' => $id,
		'provider'       => 'rankmath',
		'sources'        => $normalized_sources,
		'destination'    => $destination,
		'http_code'      => $http_code,
		'status'         => $status,
		'hit_count'      => max( 0, (int) ( $record['hits'] ?? 0 ) ),
		'writable'       => $writable,
	);
	$hash_state          = $state;
	$hash_state['hit_count'] = 0;
	$state['state_hash'] = hash( 'sha256', (string) wp_json_encode( $hash_state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) );
	if ( $filter_readback ) {
		$state = apply_filters( 'wpagent_rank_math_redirection_readback', $state, $id );
		if ( ! is_array( $state ) ) {
			return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Der Weiterleitungs-Read-back ist ungültig.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
	}
	return $state;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_rank_math_redirection_record( int $redirection_id, bool $include_trashed = false, bool $filter_readback = true ) {
	$record = \RankMath\Redirections\DB::get_redirection_by_id( $redirection_id );
	if ( ! is_array( $record ) || ( ! $include_trashed && 'trashed' === ( $record['status'] ?? '' ) ) ) {
		return new WP_Error( 'wpagent_seo_redirection_not_found', __( 'SEO-Weiterleitung nicht gefunden.', 'wp-agent-companion' ), array( 'status' => 404 ) );
	}
	return wpagent_rank_math_redirection_payload( $record, $filter_readback );
}

/** @return array<int,array<string,mixed>>|WP_Error */
function wpagent_rank_math_redirection_all_payloads() {
	$result = \RankMath\Redirections\DB::get_redirections(
		array( 'orderby' => 'id', 'order' => 'ASC', 'limit' => WPAGENT_RANK_MATH_REDIRECTION_MAX_RECORDS + 1, 'paged' => 1, 'status' => 'any' )
	);
	$rows = is_array( $result['redirections'] ?? null ) ? $result['redirections'] : array();
	if ( count( $rows ) > WPAGENT_RANK_MATH_REDIRECTION_MAX_RECORDS || absint( $result['count'] ?? 0 ) > WPAGENT_RANK_MATH_REDIRECTION_MAX_RECORDS ) {
		return new WP_Error( 'wpagent_seo_redirection_limit', __( 'Die Installation enthält zu viele Weiterleitungen für einen sicheren vollständigen Konfliktvergleich.', 'wp-agent-companion' ), array( 'status' => 413 ) );
	}
	$payloads = array();
	foreach ( $rows as $row ) {
		if ( ! is_array( $row ) ) {
			return new WP_Error( 'wpagent_seo_redirection_provider_state_invalid', __( 'Rank Math lieferte einen ungültigen Weiterleitungsdatensatz.', 'wp-agent-companion' ), array( 'status' => 502 ) );
		}
		$row['sources'] = maybe_unserialize( $row['sources'] ?? null );
		$payload        = wpagent_rank_math_redirection_payload( $row, false );
		if ( is_wp_error( $payload ) ) {
			return $payload;
		}
		$payloads[] = $payload;
	}
	return $payloads;
}

/** @param array<int,array<string,mixed>> $payloads */
function wpagent_rank_math_redirection_collection_hash( array $payloads ): string {
	$state = array();
	foreach ( $payloads as $payload ) {
		$state[ (string) $payload['redirection_id'] ] = $payload['state_hash'];
	}
	ksort( $state, SORT_NUMERIC );
	return hash( 'sha256', (string) wp_json_encode( $state, JSON_UNESCAPED_SLASHES ) );
}

/** @return string|WP_Error */
function wpagent_rank_math_redirection_acquire_lock() {
	$token = wpagent_companion_acquire_option_lock( WPAGENT_RANK_MATH_REDIRECTION_LOCK_OPTION, WPAGENT_RANK_MATH_REDIRECTION_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_seo_redirection_locked', __( 'Die SEO-Weiterleitungen werden gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_rank_math_redirection_release_lock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_RANK_MATH_REDIRECTION_LOCK_OPTION, $token );
}

function wpagent_rank_math_redirection_lock_is_owned( string $token ): bool {
	return wpagent_companion_option_lock_is_owned( WPAGENT_RANK_MATH_REDIRECTION_LOCK_OPTION, $token );
}

function wpagent_rank_math_redirection_refresh_lock( string $token ): bool {
	return wpagent_companion_refresh_option_lock( WPAGENT_RANK_MATH_REDIRECTION_LOCK_OPTION, $token, WPAGENT_RANK_MATH_REDIRECTION_LOCK_TTL );
}

/**
 * The lock is cooperative rather than a provider-side transaction. Every
 * mutable path must therefore prove token ownership again immediately before
 * and after a Rank Math write or readback.
 */
function wpagent_rank_math_redirection_lock_checkpoint( string $token ): bool {
	return wpagent_rank_math_redirection_lock_is_owned( $token )
		&& wpagent_rank_math_redirection_refresh_lock( $token );
}

/** @return string|null */
function wpagent_rank_math_redirection_destination_source( string $destination ) {
	if ( '' === $destination ) {
		return null;
	}
	$home = wp_parse_url( home_url( '/' ) );
	$url  = wp_parse_url( $destination );
	if ( false === $home || false === $url || ! wpagent_rank_math_redirection_same_site_endpoint( $home, $url ) ) {
		return null;
	}
	$home_path = '/' . trim( (string) ( $home['path'] ?? '' ), '/' );
	$path      = wpagent_rank_math_redirection_canonical_path( (string) ( $url['path'] ?? '/' ) );
	if ( null === $path ) {
		return null;
	}
	if ( '/' !== $home_path && ! str_starts_with( $path . '/', $home_path . '/' ) ) {
		return null;
	}
	if ( '/' !== $home_path ) {
		$path = substr( $path, strlen( $home_path ) );
	}
	$relative = '/' . ltrim( '' === $path ? '/' : $path, '/' );
	return '/' === $relative ? '/' : untrailingslashit( $relative );
}

/** @param array<int,array<string,mixed>> $payloads @return true|WP_Error */
function wpagent_rank_math_redirection_validate_graph( array $candidate, array $payloads, int $replacing_id = 0 ) {
	if ( 'active' !== $candidate['status'] || ! $candidate['writable'] || in_array( $candidate['http_code'], array( 410, 451 ), true ) ) {
		return true;
	}
	$source      = $candidate['sources'][0]['pattern'];
	$exact       = array();
	$insensitive = array();
	foreach ( $payloads as $payload ) {
		if ( (int) $payload['redirection_id'] === $replacing_id || 'active' !== $payload['status'] || ! $payload['writable'] || in_array( $payload['http_code'], array( 410, 451 ), true ) ) {
			continue;
		}
		$existing_source      = $payload['sources'][0]['pattern'];
		$existing_destination = wpagent_rank_math_redirection_destination_source( $payload['destination'] );
		$exact[ $existing_source ][] = $existing_destination;
		if ( $payload['sources'][0]['ignore_case'] ) {
			$insensitive[ strtolower( $existing_source ) ][] = $existing_destination;
		}
	}
	$candidate_destination = wpagent_rank_math_redirection_destination_source( $candidate['destination'] );
	$exact[ $source ][]    = $candidate_destination;
	if ( $candidate['sources'][0]['ignore_case'] ) {
		$insensitive[ strtolower( $source ) ][] = $candidate_destination;
	}
	$stack = array( array( 'path' => $source, 'leaving' => false ) );
	$state = array();
	while ( array() !== $stack ) {
		$current = array_pop( $stack );
		$cursor  = $current['path'];
		$key     = strtolower( $cursor );
		if ( $current['leaving'] ) {
			$state[ $key ] = 2;
			continue;
		}
		if ( 1 === ( $state[ $key ] ?? 0 ) ) {
			return new WP_Error( 'wpagent_seo_redirection_cycle', __( 'Die Weiterleitung würde eine Schleife erzeugen.', 'wp-agent-companion' ), array( 'status' => 400 ) );
		}
		if ( 2 === ( $state[ $key ] ?? 0 ) ) {
			continue;
		}
		$state[ $key ] = 1;
		$stack[]       = array( 'path' => $cursor, 'leaving' => true );
		$destinations  = array_merge( $exact[ $cursor ] ?? array(), $insensitive[ $key ] ?? array() );
		foreach ( array_unique( $destinations, SORT_REGULAR ) as $destination ) {
			if ( null !== $destination ) {
				$stack[] = array( 'path' => $destination, 'leaving' => false );
			}
		}
	}
	return true;
}

/** @param array<int,array<string,mixed>> $payloads @return array<string,mixed>|null */
function wpagent_rank_math_redirection_source_collision( string $source, bool $ignore_case, array $payloads, int $excluding_id = 0 ): ?array {
	foreach ( $payloads as $payload ) {
		if ( (int) $payload['redirection_id'] === $excluding_id ) {
			continue;
		}
		foreach ( $payload['sources'] as $existing ) {
			$same_source = hash_equals( $source, $existing['pattern'] )
				|| ( ( $ignore_case || $existing['ignore_case'] ) && 0 === strcasecmp( $source, $existing['pattern'] ) );
			if ( 'exact' === $existing['comparison'] && $same_source ) {
				return $payload;
			}
		}
	}
	return null;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_rank_math_redirection_next( array $input, ?array $previous = null ) {
	$creating = null === $previous;
	$allowed  = array( 'source', 'destination', 'http_code', 'status', 'ignore_case' );
	$fields   = array_intersect_key( $input, array_flip( $allowed ) );
	if ( ! $creating && array() === $fields ) {
		return new WP_Error( 'wpagent_seo_redirection_no_fields', __( 'Kein zu aktualisierendes Weiterleitungsfeld übergeben.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$next = $creating
		? array( 'source' => '', 'destination' => '', 'http_code' => 301, 'status' => 'active', 'ignore_case' => false )
		: array(
			'source'      => $previous['sources'][0]['pattern'],
			'destination' => $previous['destination'],
			'http_code'   => $previous['http_code'],
			'status'      => $previous['status'],
			'ignore_case' => $previous['sources'][0]['ignore_case'],
		);
	foreach ( $fields as $field => $value ) {
		$next[ $field ] = $value;
	}
	$source = wpagent_rank_math_redirection_validate_source( $next['source'] );
	if ( is_wp_error( $source ) ) {
		return $source;
	}
	$http_code = is_int( $next['http_code'] ) ? $next['http_code'] : 0;
	if ( ! in_array( $http_code, array( 301, 302, 307, 410, 451 ), true ) ) {
		return new WP_Error( 'wpagent_seo_redirection_http_code', __( 'http_code muss 301, 302, 307, 410 oder 451 sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$status = is_string( $next['status'] ) ? $next['status'] : '';
	if ( ! in_array( $status, array( 'active', 'inactive' ), true ) ) {
		return new WP_Error( 'wpagent_seo_redirection_status', __( 'status muss active oder inactive sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$destination = wpagent_rank_math_redirection_validate_destination( $next['destination'], $http_code );
	if ( is_wp_error( $destination ) ) {
		return $destination;
	}
	return array(
		'source'      => $source,
		'destination' => $destination,
		'http_code'   => $http_code,
		'status'      => $status,
		'ignore_case' => true === $next['ignore_case'],
	);
}

/** @return array<string,mixed> */
function wpagent_rank_math_redirection_candidate_payload( int $id, array $next ): array {
	$state = array(
		'redirection_id' => $id,
		'provider'       => 'rankmath',
		'sources'        => array( array( 'pattern' => $next['source'], 'comparison' => 'exact', 'ignore_case' => $next['ignore_case'] ) ),
		'destination'    => $next['destination'],
		'http_code'      => $next['http_code'],
		'status'         => $next['status'],
		'hit_count'      => 0,
		'writable'       => true,
	);
	$state['state_hash'] = '';
	return $state;
}

/** @return bool */
function wpagent_rank_math_redirection_semantics_match( array $actual, array $candidate ): bool {
	foreach ( array( 'redirection_id', 'provider', 'sources', 'destination', 'http_code', 'status', 'writable' ) as $field ) {
		if ( $actual[ $field ] !== $candidate[ $field ] ) {
			return false;
		}
	}
	return true;
}

/** @param mixed $value @return mixed */
function wpagent_rank_math_redirection_raw_canonicalize( $value ) {
	if ( ! is_array( $value ) ) {
		return $value;
	}
	if ( ! array_is_list( $value ) ) {
		ksort( $value, SORT_STRING );
	}
	foreach ( $value as $key => $child ) {
		$value[ $key ] = wpagent_rank_math_redirection_raw_canonicalize( $child );
	}
	return $value;
}

/** @param array<string,mixed> $raw */
function wpagent_rank_math_redirection_raw_hash( array $raw ): string {
	$encoded = wp_json_encode( wpagent_rank_math_redirection_raw_canonicalize( $raw ), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	return hash( 'sha256', is_string( $encoded ) ? $encoded : serialize( $raw ) );
}

/**
 * Capture a just-written record only if the native provider stored the exact
 * intended semantics. The raw hash additionally binds unmanaged provider
 * fields such as sources, hits and timestamps before any compensation.
 *
 * @param array<string,mixed> $expected
 * @return array{redirection_id:int,state_hash:string,raw_hash:string}|null
 */
function wpagent_rank_math_redirection_capture_owned_candidate( int $redirection_id, array $expected ): ?array {
	$raw = wpagent_rank_math_redirection_load_raw( $redirection_id, true );
	if ( is_wp_error( $raw ) ) {
		return null;
	}
	$payload = wpagent_rank_math_redirection_payload( $raw, false );
	if ( is_wp_error( $payload ) || ! wpagent_rank_math_redirection_semantics_match( $payload, $expected ) ) {
		return null;
	}
	return array(
		'redirection_id' => $redirection_id,
		'state_hash'     => $payload['state_hash'],
		'raw_hash'       => wpagent_rank_math_redirection_raw_hash( $raw ),
	);
}

/** @param array{redirection_id:int,state_hash:string,raw_hash:string} $candidate */
function wpagent_rank_math_redirection_candidate_is_current( array $candidate ): bool {
	$redirection_id = absint( $candidate['redirection_id'] ?? 0 );
	$state_hash     = is_string( $candidate['state_hash'] ?? null ) ? $candidate['state_hash'] : '';
	$raw_hash       = is_string( $candidate['raw_hash'] ?? null ) ? $candidate['raw_hash'] : '';
	if ( $redirection_id < 1 || ! preg_match( '/^[a-f0-9]{64}$/D', $state_hash ) || ! preg_match( '/^[a-f0-9]{64}$/D', $raw_hash ) ) {
		return false;
	}
	$raw = wpagent_rank_math_redirection_load_raw( $redirection_id, true );
	if ( is_wp_error( $raw ) ) {
		return false;
	}
	$payload = wpagent_rank_math_redirection_payload( $raw, false );
	return ! is_wp_error( $payload )
		&& hash_equals( $state_hash, $payload['state_hash'] )
		&& hash_equals( $raw_hash, wpagent_rank_math_redirection_raw_hash( $raw ) );
}

/** @return WP_Error */
function wpagent_rank_math_redirection_recovery_required( string $reason, int $redirection_id = 0 ) {
	$data = array( 'status' => 500, 'recovery_required' => true, 'rollback_attempted' => false, 'reason' => $reason );
	if ( $redirection_id > 0 ) {
		$current = wpagent_rank_math_redirection_record( $redirection_id, true, false );
		if ( is_array( $current ) && is_string( $current['state_hash'] ?? null ) ) {
			$data['redirection_id'] = $redirection_id;
			$data['current_hash']   = $current['state_hash'];
		}
	}
	return new WP_Error( 'wpagent_seo_redirection_recovery_required', __( 'Der Rank-Math-Weiterleitungszustand ist nach einer unsicheren Änderung nicht automatisch kompensierbar. Prüfe Rank Math im WordPress-Admin.', 'wp-agent-companion' ), $data );
}

/**
 * Compensate only the exact candidate this operation wrote. Rank Math exposes
 * no conditional write/delete primitive, therefore an external editor change,
 * a hit-counter update or a lost lock always wins over automatic cleanup.
 *
 * @param array<string,mixed>|null $previous_raw Null means a failed create.
 * @param array{redirection_id:int,state_hash:string,raw_hash:string}|null $candidate
 * @return WP_Error
 */
function wpagent_rank_math_redirection_rollback_error( ?array $previous_raw, ?array $candidate, string $token, string $reason ) {
	$redirection_id = is_array( $candidate ) ? absint( $candidate['redirection_id'] ?? 0 ) : 0;
	if ( ! is_array( $candidate )
		|| ! wpagent_rank_math_redirection_lock_checkpoint( $token )
		|| ! wpagent_perm_rank_math_redirections()
		|| ! wpagent_rank_math_redirection_candidate_is_current( $candidate )
		|| ! wpagent_rank_math_redirection_lock_checkpoint( $token )
		|| ! wpagent_perm_rank_math_redirections() ) {
		return wpagent_rank_math_redirection_recovery_required( $reason, $redirection_id );
	}

	$verified = false;
	try {
		if ( null === $previous_raw ) {
			$deleted  = \RankMath\Redirections\DB::delete( array( $redirection_id ) );
			$verified = false !== $deleted
				&& wpagent_rank_math_redirection_lock_checkpoint( $token )
				&& wpagent_perm_rank_math_redirections()
				&& is_wp_error( wpagent_rank_math_redirection_load_raw( $redirection_id, true ) );
		} else {
			$previous_id = absint( $previous_raw['id'] ?? 0 );
			if ( $previous_id < 1 || $previous_id !== $redirection_id ) {
				return wpagent_rank_math_redirection_recovery_required( $reason, $redirection_id );
			}
			$item   = \RankMath\Redirections\Redirection::from( $previous_raw );
			$saved  = $item->save();
			$fresh  = wpagent_rank_math_redirection_load_raw( $redirection_id, true );
			$expect = wpagent_rank_math_redirection_payload( $previous_raw, false );
			$actual = is_wp_error( $fresh ) ? $fresh : wpagent_rank_math_redirection_payload( $fresh, false );
			$verified = $redirection_id === absint( $saved )
				&& ! is_wp_error( $fresh )
				&& ! is_wp_error( $expect )
				&& ! is_wp_error( $actual )
				&& hash_equals( $expect['state_hash'], $actual['state_hash'] )
				&& hash_equals( wpagent_rank_math_redirection_raw_hash( $previous_raw ), wpagent_rank_math_redirection_raw_hash( $fresh ) )
				&& wpagent_rank_math_redirection_lock_checkpoint( $token )
				&& wpagent_perm_rank_math_redirections();
		}
	} catch ( Throwable ) {
		$verified = false;
	}
	if ( ! $verified ) {
		return wpagent_rank_math_redirection_recovery_required( $reason, $redirection_id );
	}
	return new WP_Error( 'wpagent_seo_redirection_write_unverified', __( 'Rank Math hat die Änderung nicht exakt bestätigt. Der vorherige Zustand wurde verifiziert wiederhergestellt.', 'wp-agent-companion' ), array( 'status' => 502, 'rollback_verified' => true ) );
}

/** @return array<string,mixed> */
function wpagent_rank_math_redirection_source_schema(): array {
	return array(
		'type'                 => 'object',
		'properties'           => array(
			'pattern'     => array( 'type' => 'string' ),
			'comparison'  => array( 'type' => 'string', 'enum' => array( 'exact', 'contains', 'start', 'end', 'regex' ) ),
			'ignore_case' => array( 'type' => 'boolean' ),
		),
		'required'             => array( 'pattern', 'comparison', 'ignore_case' ),
		'additionalProperties' => false,
	);
}

/** @return array<string,mixed> */
function wpagent_rank_math_redirection_schema(): array {
	return array(
		'type'                 => 'object',
		'properties'           => array(
			'redirection_id' => array( 'type' => 'integer', 'minimum' => 1 ),
			'provider'       => array( 'type' => 'string', 'enum' => array( 'rankmath' ) ),
			'sources'        => array( 'type' => 'array', 'items' => wpagent_rank_math_redirection_source_schema(), 'minItems' => 1, 'maxItems' => 20 ),
			'destination'    => array( 'type' => 'string' ),
			'http_code'      => array( 'type' => 'integer', 'enum' => array( 301, 302, 307, 410, 451 ) ),
			'status'         => array( 'type' => 'string', 'enum' => array( 'active', 'inactive' ) ),
			'hit_count'      => array( 'type' => 'integer', 'minimum' => 0 ),
			'writable'       => array( 'type' => 'boolean' ),
			'state_hash'     => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
		),
		'required'             => array( 'redirection_id', 'provider', 'sources', 'destination', 'http_code', 'status', 'hit_count', 'writable', 'state_hash' ),
		'additionalProperties' => false,
	);
}
