<?php
/**
 * Exact user-role storage safety for the legacy core mutators.
 *
 * Kept separate from the general core-mutator helpers because this is the
 * only path that needs to bind one usermeta row at storage level.
 *
 * @package WPAgentCompanion
 */

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

/** @return array{id:int,login:string,email:string,registered:string,roles:array<int,string>} */
function wpagent_core_mutator_user_identity( WP_User $user ): array {
	$roles = array_values( array_filter( $user->roles, 'is_string' ) );
	sort( $roles, SORT_STRING );
	return array(
		'id'         => (int) $user->ID,
		'login'      => (string) $user->user_login,
		'email'      => (string) $user->user_email,
		'registered' => (string) $user->user_registered,
		'roles'      => $roles,
	);
}

/** @return array{identity:array{id:int,login:string,email:string,registered:string,roles:array<int,string>},hash:string} */
function wpagent_core_mutator_user_identity_state( WP_User $user ): array {
	$state = array( 'identity' => wpagent_core_mutator_user_identity( $user ) );
	$state['hash'] = wpagent_core_mutator_hash( array_merge( array( 'scope' => 'user-identity' ), $state ) );
	return $state;
}

/**
 * Reads the one exact usermeta row that WordPress uses for site roles.
 *
 * A missing or duplicated row deliberately is not repaired or guessed. Core's
 * update_user_meta() may add or update multiple matching rows, neither of
 * which is a safe compare-and-swap primitive for an externally editable
 * account.
 *
 * @return array{readable:bool,exact:bool,key:string,umeta_id:int|null,raw:string|null}
 */
function wpagent_core_mutator_user_capability_row( int $user_id ): array {
	global $wpdb;
	$key  = $wpdb->get_blog_prefix() . 'capabilities';
	$rows = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT umeta_id, meta_value FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key = %s ORDER BY umeta_id ASC LIMIT 2",
			$user_id,
			$key
		),
		ARRAY_A
	);
	if ( '' !== (string) $wpdb->last_error || ! is_array( $rows ) ) {
		return array( 'readable' => false, 'exact' => false, 'key' => $key, 'umeta_id' => null, 'raw' => null );
	}
	if ( 1 !== count( $rows ) || ! isset( $rows[0]['umeta_id'], $rows[0]['meta_value'] ) || ! is_string( $rows[0]['meta_value'] ) ) {
		return array( 'readable' => true, 'exact' => false, 'key' => $key, 'umeta_id' => null, 'raw' => null );
	}
	return array(
		'readable' => true,
		'exact'    => true,
		'key'      => $key,
		'umeta_id' => (int) $rows[0]['umeta_id'],
		'raw'      => $rows[0]['meta_value'],
	);
}

/**
 * Hashes the complete role write target, including the storage row identity
 * and raw serialized capabilities value that the later CAS must still own.
 *
 * @return array{identity:array{id:int,login:string,email:string,registered:string,roles:array<int,string>},capabilities:array{readable:bool,exact:bool,key:string,umeta_id:int|null,raw:string|null},hash:string}
 */
function wpagent_core_mutator_user_role_state( WP_User $user ): array {
	$state = array(
		'identity'     => wpagent_core_mutator_user_identity( $user ),
		'capabilities' => wpagent_core_mutator_user_capability_row( (int) $user->ID ),
	);
	$state['hash'] = wpagent_core_mutator_hash( array_merge( array( 'scope' => 'user-role' ), $state ) );
	return $state;
}

/** @return string|WP_Error */
function wpagent_core_mutator_user_role_candidate_raw( WP_User $user, array $capability_row, string $role ) {
	if ( empty( $capability_row['exact'] ) || ! is_string( $capability_row['raw'] ?? null ) ) {
		return new WP_Error( 'wpagent_user_capability_storage_unavailable', __( 'Die exakte Capabilities-Metazeile des Nutzers ist nicht verfügbar. Es wurde nichts geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	$capabilities = maybe_unserialize( $capability_row['raw'] );
	if ( ! is_array( $capabilities ) ) {
		return new WP_Error( 'wpagent_user_capability_storage_invalid', __( 'Die Capabilities-Metazeile des Nutzers ist ungültig. Es wurde nichts geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	// Mirrors WP_User::set_role(): retain explicit non-role capabilities.
	foreach ( $user->roles as $old_role ) {
		if ( is_string( $old_role ) ) {
			unset( $capabilities[ $old_role ] );
		}
	}
	$capabilities[ $role ] = true;
	$candidate = maybe_serialize( $capabilities );
	if ( ! is_string( $candidate ) ) {
		return new WP_Error( 'wpagent_user_capability_candidate_invalid', __( 'Die neue Capabilities-Metazeile konnte nicht exakt gebunden werden. Es wurde nichts geändert.', 'wp-agent-companion' ), array( 'status' => 409 ) );
	}
	return $candidate;
}

/**
 * Atomically replaces exactly one known capabilities row only while both its
 * row identity and full prior raw value still match. A zero-row result is a
 * conflict, never an invitation to overwrite or add a replacement row.
 */
function wpagent_core_mutator_replace_user_capabilities_if_current( int $user_id, array $before, string $candidate_raw ): bool {
	if ( empty( $before['exact'] ) || ! is_int( $before['umeta_id'] ?? null ) || ! is_string( $before['key'] ?? null ) || ! is_string( $before['raw'] ?? null ) ) {
		return false;
	}
	global $wpdb;
	$updated = $wpdb->query(
		$wpdb->prepare(
			"UPDATE {$wpdb->usermeta} SET meta_value = %s WHERE umeta_id = %d AND user_id = %d AND meta_key = %s AND meta_value = %s",
			$candidate_raw,
			$before['umeta_id'],
			$user_id,
			$before['key'],
			$before['raw']
		)
	);
	return 1 === (int) $updated;
}

/** @param array{id:int,login:string,email:string,registered:string,roles:array<int,string>} $left @param array{id:int,login:string,email:string,registered:string,roles:array<int,string>} $right */
function wpagent_core_mutator_same_user_principal( array $left, array $right ): bool {
	return $left['id'] === $right['id']
		&& hash_equals( $left['login'], $right['login'] )
		&& hash_equals( $left['email'], $right['email'] )
		&& hash_equals( $left['registered'], $right['registered'] );
}
