<?php
/** Lock, raw-row CAS and fail-closed recovery helpers for Astra writes. */

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

/** @return string|WP_Error */
function wpagent_astra_acquire_lock() {
	$token = wpagent_companion_acquire_option_lock( WPAGENT_ASTRA_LOCK_OPTION, WPAGENT_ASTRA_LOCK_TTL );
	if ( is_string( $token ) ) {
		return $token;
	}
	return new WP_Error( 'wpagent_astra_locked', __( 'Die Astra-Einstellungen werden gerade durch einen anderen Vorgang geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
}

function wpagent_astra_release_lock( string $token ): void {
	wpagent_companion_release_option_lock( WPAGENT_ASTRA_LOCK_OPTION, $token );
}

function wpagent_astra_lock_is_owned( string $token ): bool {
	return wpagent_companion_option_lock_is_owned( WPAGENT_ASTRA_LOCK_OPTION, $token );
}

function wpagent_astra_refresh_lock( string $token ): bool {
	return wpagent_companion_refresh_option_lock( WPAGENT_ASTRA_LOCK_OPTION, $token, WPAGENT_ASTRA_LOCK_TTL );
}

/** @return true|WP_Error */
function wpagent_astra_checkpoint( string $token, bool $write_started = false ) {
	if ( current_user_can( 'manage_options' ) && wpagent_astra_refresh_lock( $token ) && wpagent_astra_lock_is_owned( $token ) ) {
		return true;
	}
	return new WP_Error(
		$write_started ? 'wpagent_astra_lock_lost_after_write' : 'wpagent_astra_lock_lost',
		__( 'Die Astra-Änderung hat ihre exklusive Sperre verloren.', 'wp-agent-companion' ),
		array( 'status' => $write_started ? 500 : 409, 'write_started' => $write_started )
	);
}

/** @param array{exists:bool,raw:string,autoload:string} $expected */
function wpagent_astra_option_row_matches( string $option_name, array $expected ): bool {
	$row = wpagent_companion_read_option_row_direct( $option_name );
	if ( ! $expected['exists'] ) {
		return ! is_array( $row );
	}
	return is_array( $row )
		&& hash_equals( $expected['raw'], $row['raw'] )
		&& hash_equals( $expected['autoload'], $row['autoload'] );
}

/**
 * Restores one row only if the exact candidate written by this operation still
 * exists. The option API cannot express a raw-value and autoload conditional
 * update, so using it here could overwrite a Customizer change.
 *
 * @param array{exists:bool,raw:string,autoload:string} $previous
 * @param array{exists:bool,raw:string,autoload:string} $candidate
 */
function wpagent_astra_restore_option_candidate( string $option_name, array $previous, array $candidate ): bool {
	global $wpdb;
	if ( ! wpagent_astra_option_row_matches( $option_name, $candidate ) ) {
		return false;
	}
	if ( ! $candidate['exists'] && ! $previous['exists'] ) {
		return true;
	}
	if ( $candidate['exists'] && ! $previous['exists'] ) {
		$changed = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->options} WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND autoload = %s",
				$option_name,
				$candidate['raw'],
				$candidate['autoload']
			)
		);
	} elseif ( ! $candidate['exists'] ) {
		$changed = $wpdb->query(
			$wpdb->prepare(
				"INSERT INTO {$wpdb->options} (option_name, option_value, autoload) SELECT %s, %s, %s WHERE NOT EXISTS (SELECT 1 FROM {$wpdb->options} WHERE option_name = %s)",
				$option_name,
				$previous['raw'],
				$previous['autoload'],
				$option_name
			)
		);
	} else {
		$changed = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s, autoload = %s WHERE option_name = %s AND HEX(option_value) = HEX(%s) AND autoload = %s",
				$previous['raw'],
				$previous['autoload'],
				$option_name,
				$candidate['raw'],
				$candidate['autoload']
			)
		);
	}
	if ( 1 !== $changed ) {
		return false;
	}
	wpagent_companion_forget_option_cache(
		$option_name,
		in_array( $previous['autoload'], wp_autoload_values_to_autoload(), true ) || in_array( $candidate['autoload'], wp_autoload_values_to_autoload(), true )
	);
	return wpagent_astra_option_row_matches( $option_name, $previous );
}

/**
 * Restores only an exact candidate under an owned, renewed token. Astra's
 * native abilities can write unknown settings, so normal provider-readback
 * failures deliberately use recovery_required instead of this helper.
 *
 * @param array{exists:bool,value:array,raw:string,autoload:string,palette_exists:bool,palette_value:array,palette_raw:string,palette_autoload:string} $snapshot
 * @param array{exists:bool,value:array,raw:string,autoload:string,palette_exists:bool,palette_value:array,palette_raw:string,palette_autoload:string} $candidate
 * @return true|WP_Error
 */
function wpagent_astra_rollback( array $snapshot, array $candidate, string $expected_hash, string $token ) {
	if ( is_wp_error( wpagent_astra_checkpoint( $token, true ) ) ) {
		return new WP_Error( 'wpagent_astra_rollback_unverified', __( 'Das Astra-Rollback konnte nicht sicher gestartet werden.', 'wp-agent-companion' ), array( 'status' => 500, 'rollback_verified' => false ) );
	}
	$previous_settings  = array( 'exists' => $snapshot['exists'], 'raw' => $snapshot['raw'], 'autoload' => $snapshot['autoload'] );
	$candidate_settings = array( 'exists' => $candidate['exists'], 'raw' => $candidate['raw'], 'autoload' => $candidate['autoload'] );
	$previous_palette   = array( 'exists' => $snapshot['palette_exists'], 'raw' => $snapshot['palette_raw'], 'autoload' => $snapshot['palette_autoload'] );
	$candidate_palette  = array( 'exists' => $candidate['palette_exists'], 'raw' => $candidate['palette_raw'], 'autoload' => $candidate['palette_autoload'] );
	if ( ! wpagent_astra_option_row_matches( 'astra-settings', $candidate_settings ) || ! wpagent_astra_option_row_matches( 'astra-color-palettes', $candidate_palette ) ) {
		return new WP_Error( 'wpagent_astra_rollback_unverified', __( 'Astra wurde nach dem Schreibvorgang erneut geändert. Der aktuelle Zustand bleibt erhalten.', 'wp-agent-companion' ), array( 'status' => 500, 'rollback_verified' => false, 'recovery_required' => true ) );
	}
	if ( ! wpagent_astra_restore_option_candidate( 'astra-settings', $previous_settings, $candidate_settings ) || ! wpagent_astra_restore_option_candidate( 'astra-color-palettes', $previous_palette, $candidate_palette ) ) {
		return new WP_Error( 'wpagent_astra_rollback_unverified', __( 'Das Astra-Rollback konnte nicht vollständig eigentumsgeprüft ausgeführt werden. Der aktuelle Zustand bleibt erhalten.', 'wp-agent-companion' ), array( 'status' => 500, 'rollback_verified' => false, 'recovery_required' => true ) );
	}
	$fresh = wpagent_astra_snapshot();
	if ( is_wp_error( $fresh ) || ! hash_equals( $expected_hash, wpagent_astra_state_hash( $fresh ) ) ) {
		return new WP_Error( 'wpagent_astra_rollback_unverified', __( 'Das Astra-Rollback konnte nicht verifiziert werden. Prüfe die Theme-Einstellungen sofort manuell.', 'wp-agent-companion' ), array( 'status' => 500, 'rollback_verified' => false, 'recovery_required' => true ) );
	}
	return true;
}

/** @return WP_Error */
function wpagent_astra_recovery_required() {
	$data  = array( 'status' => 502, 'rollback_verified' => false, 'recovery_required' => true );
	$fresh = wpagent_astra_snapshot();
	if ( is_array( $fresh ) ) {
		$data['current_hash'] = wpagent_astra_state_hash( $fresh );
	}
	return new WP_Error(
		'wpagent_astra_recovery_required',
		__( 'Astra hat die Änderung nicht vollständig bestätigt. Es wurde kein automatisches Rollback ausgeführt, weil ein fremder Customizer-Schreibvorgang nicht sicher ausgeschlossen werden kann. Wiederhole den Vorgang nicht automatisch und prüfe den aktuellen Zustand manuell.', 'wp-agent-companion' ),
		$data
	);
}

/** @return WP_Error */
function wpagent_astra_fail_after_rollback( array $snapshot, string $previous_hash ) {
	unset( $snapshot, $previous_hash );
	return wpagent_astra_recovery_required();
}
