<?php
/**
 * Ability: wp-agent/set-user-role
 *
 * Setzt die Rolle eines bestehenden Nutzers direkt (ersetzt ALLE bisherigen
 * Rollen durch genau eine, über einen bedingten Austausch genau der
 * Capabilities-Metazeile). Nutzt die Capability
 * promote_users statt der breiteren edit_users: das ist die präzise
 * WordPress-Capability, an die der Kern selbst Rollenänderungen bindet
 * (siehe wp-admin/users.php, Bulk-Aktion "Rolle ändern zu"), unabhängig
 * davon, welche sonstigen Profilfelder edit_users freischaltet.
 *
 * Zwei harte, nicht per Flag umgehbare Schutzmechanismen:
 *  - User-ID 1 (die ursprüngliche Administrator-Kennung fast jeder
 *    WordPress-Installation) darf nie umgeschrieben werden.
 *  - Der aufrufende Account selbst (get_current_user_id()) darf seine eigene
 *    Rolle nie ändern. Da die Abilities API immer im Kontext des per
 *    Application Password authentifizierten Nutzers läuft, IST dieser
 *    Nutzer im produktiven Einsatz der Companion-Bot; die Selbst-Sperre
 *    schützt also genau den in der Aufgabenstellung geforderten
 *    "Companion-Bot darf sich nicht selbst anfassen"-Fall, ohne einen
 *    fest einprogrammierten Benutzernamen vorauszusetzen.
 *
 * Zusätzliches weiches Gate: Rollen, deren Capability-Satz manage_options
 * enthält (das ist administrator-äquivalent), werden nur mit dem
 * expliziten Input-Flag i_grant_admin=true akzeptiert, sonst 403.
 *
 * @package WPAgentCompanion
 */

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

function wpagent_ability_set_user_role(): array {
	return array(
		'label'               => __( 'Nutzerrolle setzen', 'wp-agent-companion' ),
		'description'         => __( 'Setzt die Rolle eines bestehenden Nutzers direkt (ersetzt alle bisherigen Rollen durch genau eine). Administrator-äquivalente Rollen (Capability manage_options) erfordern das explizite Flag i_grant_admin=true, sonst 403. User-ID 1 und der aufrufende Account selbst dürfen nie geändert werden. Re-Read-verifiziert.', 'wp-agent-companion' ),
		'category'            => 'wp-agent',
		'input_schema'        => array(
			'type'       => 'object',
			'properties' => array(
				'user_id'       => array( 'type' => 'integer', 'minimum' => 2 ),
				'role'          => array(
					'type'        => 'string',
					'minLength'   => 1,
					'maxLength'   => 64,
					'pattern'     => '^[a-z0-9_-]+$',
					'description' => __( 'Muss eine registrierte Rolle sein (wp_roles()).', 'wp-agent-companion' ),
				),
				'i_grant_admin' => array(
					'type'        => 'boolean',
					'default'     => false,
					'description' => __( 'Pflicht (true), wenn die Zielrolle die Capability manage_options besitzt (z. B. administrator).', 'wp-agent-companion' ),
				),
				'expected_hash'  => array(
					'type'    => 'string',
					'pattern' => '^[a-f0-9]{64}$',
				),
			),
			'required'              => array( 'user_id', 'role', 'expected_hash' ),
			'additionalProperties'  => false,
		),
		'output_schema'       => array(
			'type'       => 'object',
			'properties' => array(
				'user_id'  => array( 'type' => 'integer' ),
				'role'     => array( 'type' => 'string' ),
				'verified' => array( 'type' => 'boolean' ),
				'previous_hash' => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
				'state_hash'    => array( 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ),
			),
			'required'              => array( 'user_id', 'role', 'verified', 'previous_hash', 'state_hash' ),
			'additionalProperties'  => false,
		),
		'execute_callback'    => 'wpagent_exec_set_user_role',
		'permission_callback' => 'wpagent_perm_promote_users',
		'meta'                => array(
			'show_in_rest' => true,
			'annotations'  => array(
				'readonly'    => false,
				'destructive' => false,
				'idempotent'  => false,
			),
		),
	);
}

/**
 * Permission callback: der Aufrufer muss promote_users besitzen. Diese
 * Capability ist in einer Standardinstallation Administratoren vorbehalten;
 * der als Editor eingerichtete Companion-Bot besitzt sie nie.
 *
 * @param mixed $input Unused ability input.
 * @return bool
 */
function wpagent_perm_promote_users( $input = null ): bool {
	return current_user_can( 'promote_users' );
}

/**
 * Prüft, ob eine Rolle Administrator-äquivalente Rechte trägt: entweder
 * die Rolle "administrator" selbst, oder jede Rolle, deren Capability-Satz
 * manage_options enthält. Bewusst über die Capability statt nur über den
 * Rollen-Slug geprüft, damit auch eine individuell benannte Rolle mit
 * manage_options nicht am Gate vorbeikommt.
 *
 * @param string $role
 * @return bool
 */
function wpagent_role_grants_admin_capability( string $role ): bool {
	if ( 'administrator' === $role ) {
		return true;
	}
	$role_object = get_role( $role );
	if ( ! $role_object instanceof WP_Role ) {
		return false;
	}
	return ! empty( $role_object->capabilities['manage_options'] );
}

/**
 * @param mixed $input
 * @return array|WP_Error
 */
function wpagent_exec_set_user_role( $input ) {
	$input   = is_array( $input ) ? $input : array();
	if ( ! wpagent_core_mutator_is_strict_integer( $input['user_id'] ?? null ) || (int) $input['user_id'] < 2 ) {
		return new WP_Error( 'wpagent_bad_user_id', __( 'user_id muss eine positive, nicht geschützte Ganzzahl sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$user_id = (int) $input['user_id'];
	if ( ! current_user_can( 'promote_users' ) || ! current_user_can( 'promote_user', $user_id ) ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}
	$user = get_userdata( $user_id );
	if ( ! $user ) {
		return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
	}

	$role = $input['role'] ?? '';
	if ( ! is_string( $role ) || '' === $role || strlen( $role ) > 64 || 1 !== preg_match( '/^[a-z0-9_-]+$/D', $role ) || ! wp_roles()->is_role( $role ) ) {
		return new WP_Error(
			'wpagent_bad_role',
			sprintf( __( 'Ungültige Rolle "%s".', 'wp-agent-companion' ), $role ),
			array( 'status' => 400 )
		);
	}

	if ( 1 === $user_id ) {
		return new WP_Error(
			'wpagent_protected_user',
			__( 'User-ID 1 ist geschützt und darf nicht geändert werden.', 'wp-agent-companion' ),
			array( 'status' => 400 )
		);
	}
	if ( get_current_user_id() === $user_id ) {
		return new WP_Error(
			'wpagent_protected_user',
			__( 'Der aufrufende Account darf seine eigene Rolle nicht ändern.', 'wp-agent-companion' ),
			array( 'status' => 400 )
		);
	}
	$expected_hash = wpagent_core_mutator_expected_hash( $input );
	if ( is_wp_error( $expected_hash ) ) {
		return $expected_hash;
	}

	if ( array_key_exists( 'i_grant_admin', $input ) && ! is_bool( $input['i_grant_admin'] ) ) {
		return new WP_Error( 'wpagent_bad_admin_grant', __( 'i_grant_admin muss ein Boolean sein.', 'wp-agent-companion' ), array( 'status' => 400 ) );
	}
	$i_grant_admin = true === ( $input['i_grant_admin'] ?? false );
	if ( wpagent_role_grants_admin_capability( $role ) && ! $i_grant_admin ) {
		return new WP_Error(
			'wpagent_admin_grant_required',
			__( 'Diese Rolle besitzt Administrator-Rechte (manage_options). Setze i_grant_admin=true, um das explizit zu bestätigen.', 'wp-agent-companion' ),
			array( 'status' => 403 )
		);
	}

	$before = wpagent_core_mutator_user_role_state( $user );
	if ( ! hash_equals( $before['hash'], $expected_hash ) ) {
		return new WP_Error( 'wpagent_user_conflict', __( 'Der Nutzer wurde seit dem Lesen verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $before['hash'] ) );
	}
	$lock = wpagent_core_mutator_acquire_lock( 'user', (string) $user_id );
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}

	try {
		if ( ! wpagent_core_mutator_refresh_lock( 'user', (string) $user_id, $lock ) || ! current_user_can( 'promote_users' ) || ! current_user_can( 'promote_user', $user_id ) ) {
			return new WP_Error( 'wpagent_user_lock_lost', __( 'Die exklusive Sperre oder Berechtigung ging vor dem Rollenwechsel verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		$fresh_before = get_userdata( $user_id );
		if ( ! $fresh_before instanceof WP_User ) {
			return new WP_Error( 'wpagent_forbidden', __( 'Keine Berechtigung.', 'wp-agent-companion' ), array( 'status' => 403 ) );
		}
		$locked_before = wpagent_core_mutator_user_role_state( $fresh_before );
		if ( ! hash_equals( $locked_before['hash'], $expected_hash ) ) {
			return new WP_Error( 'wpagent_user_conflict', __( 'Der Nutzer wurde während der Sperrübernahme verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $locked_before['hash'] ) );
		}
		if ( empty( $locked_before['capabilities']['readable'] ) || empty( $locked_before['capabilities']['exact'] ) ) {
			return new WP_Error( 'wpagent_user_capability_storage_unavailable', __( 'Die Capabilities-Metazeile des Nutzers ist nicht eindeutig lesbar. Ändere die Rolle manuell in WordPress.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => $locked_before['hash'] ) );
		}
		if ( ! wp_roles()->is_role( $role ) || ( wpagent_role_grants_admin_capability( $role ) && ! $i_grant_admin ) ) {
			return new WP_Error( 'wpagent_admin_grant_required', __( 'Die Zielrolle ist nicht mehr zulässig oder erfordert i_grant_admin=true.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( array( $role ) === $locked_before['identity']['roles'] ) {
			if ( ! wpagent_core_mutator_lock_is_owned( 'user', (string) $user_id, $lock ) || ! current_user_can( 'promote_users' ) || ! current_user_can( 'promote_user', $user_id ) ) {
				return new WP_Error( 'wpagent_user_lock_lost', __( 'Die exklusive Sperre oder Berechtigung ging während der No-op-Prüfung verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
			}
			return array(
				'user_id'       => $user_id,
				'role'          => $role,
				'verified'      => true,
				'previous_hash' => $locked_before['hash'],
				'state_hash'    => $locked_before['hash'],
			);
		}
		$candidate_raw = wpagent_core_mutator_user_role_candidate_raw( $fresh_before, $locked_before['capabilities'], $role );
		if ( is_wp_error( $candidate_raw ) ) {
			return $candidate_raw;
		}
		if ( ! wpagent_core_mutator_refresh_lock( 'user', (string) $user_id, $lock ) || ! current_user_can( 'promote_users' ) || ! current_user_can( 'promote_user', $user_id ) ) {
			return new WP_Error( 'wpagent_user_lock_lost', __( 'Die exklusive Sperre oder Berechtigung ging unmittelbar vor dem Rollenwechsel verloren.', 'wp-agent-companion' ), array( 'status' => 409 ) );
		}
		if ( ! wpagent_core_mutator_replace_user_capabilities_if_current( $user_id, $locked_before['capabilities'], $candidate_raw ) ) {
			clean_user_cache( $user_id );
			$after_failed = get_userdata( $user_id );
			$failed_state = $after_failed instanceof WP_User ? wpagent_core_mutator_user_role_state( $after_failed ) : null;
			if ( is_array( $failed_state ) && hash_equals( $locked_before['hash'], $failed_state['hash'] ) ) {
				return new WP_Error( 'wpagent_user_role_write_failed', __( 'Die Capabilities-Metazeile konnte nicht bedingt aktualisiert werden.', 'wp-agent-companion' ), array( 'status' => 500, 'recovery_required' => false ) );
			}
			return new WP_Error( 'wpagent_user_conflict', __( 'Die Rolle wurde vor dem bedingten Schreiben durch einen anderen Vorgang verändert.', 'wp-agent-companion' ), array( 'status' => 409, 'current_hash' => is_array( $failed_state ) ? $failed_state['hash'] : '' ) );
		}
		clean_user_cache( $user_id );
		$fresh = get_userdata( $user_id );
		$after = $fresh instanceof WP_User ? wpagent_core_mutator_user_role_state( $fresh ) : null;
		$verified = is_array( $after )
			&& wpagent_core_mutator_same_user_principal( $locked_before['identity'], $after['identity'] )
			&& array( $role ) === $after['identity']['roles']
			&& ! empty( $after['capabilities']['exact'] )
			&& is_string( $after['capabilities']['raw'] )
			&& hash_equals( $candidate_raw, $after['capabilities']['raw'] )
			&& wpagent_core_mutator_lock_is_owned( 'user', (string) $user_id, $lock )
			&& current_user_can( 'promote_users' )
			&& current_user_can( 'promote_user', $user_id );
		if ( ! $verified ) {
			return wpagent_core_mutator_recovery_required( 'wpagent_user_role_recovery_required', __( 'Der Rollenwechsel konnte nicht vollständig verifiziert werden. Es wurde kein automatisches Rollback ausgeführt, weil Rollen und Einzel-Capabilities eines fremden Schreibvorgangs nicht sicher getrennt werden können.', 'wp-agent-companion' ), is_array( $after ) ? $after['hash'] : '' );
		}

		return array(
			'user_id'       => $user_id,
			'role'          => $role,
			'verified'      => true,
			'previous_hash' => $locked_before['hash'],
			'state_hash'    => $after['hash'],
		);
	} finally {
		wpagent_core_mutator_release_lock( 'user', (string) $user_id, $lock );
	}
}
