<?php
/** RFC-8628 Device Authorization Grant for the wpagently OAuth server. */

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

function wpagent_companion_oauth_device_verification_uri(): string {
	return add_query_arg( 'page', 'wpagently-oauth-device', admin_url( 'admin.php' ) );
}

/** @return string|WP_Error */
function wpagent_companion_oauth_normalize_device_user_code( $value ) {
	if ( ! is_string( $value ) || strlen( $value ) > 32 ) {
		return new WP_Error( 'invalid_user_code', __( 'Der Gerätecode ist ungültig oder abgelaufen.', 'wp-agent-companion' ) );
	}
	$code = strtoupper( str_replace( ' ', '', trim( $value ) ) );
	if ( ! preg_match( '/^[A-Z2-9]{4}-[A-Z2-9]{4}$/', $code ) ) {
		return new WP_Error( 'invalid_user_code', __( 'Der Gerätecode ist ungültig oder abgelaufen.', 'wp-agent-companion' ) );
	}
	return $code;
}

function wpagent_companion_oauth_new_device_user_code(): string {
	$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
	$code     = '';
	for ( $index = 0; $index < 8; $index++ ) {
		$code .= $alphabet[ random_int( 0, strlen( $alphabet ) - 1 ) ];
	}
	return substr( $code, 0, 4 ) . '-' . substr( $code, 4 );
}

/** @return array<string,mixed>|WP_Error */
function wpagent_companion_oauth_validate_device_authorization_request( array $params ) {
	$client_id = is_string( $params['client_id'] ?? null ) && strlen( $params['client_id'] ) <= 512 ? $params['client_id'] : '';
	$client    = wpagent_companion_oauth_clients()[ $client_id ] ?? null;
	if ( ! is_array( $client ) || '' !== (string) ( $client['revoked_at'] ?? '' ) || ! wpagent_companion_oauth_client_allows_grant( $client, WPAGENT_COMPANION_OAUTH_DEVICE_GRANT ) ) {
		return new WP_Error( 'invalid_client', __( 'Unbekannter OAuth-Client.', 'wp-agent-companion' ) );
	}
	$resource = is_string( $params['resource'] ?? null ) && strlen( $params['resource'] ) <= 2048 ? untrailingslashit( $params['resource'] ) : '';
	if ( is_string( $params['resource'] ?? null ) && strlen( $params['resource'] ) > 2048 ) {
		return new WP_Error( 'invalid_target', __( 'Der angeforderte OAuth-Endpunkt ist nicht erlaubt.', 'wp-agent-companion' ) );
	}
	if ( '' === $resource ) {
		$resource = wpagent_companion_oauth_mcp_resource();
	}
	if ( ! in_array( $resource, wpagent_companion_oauth_resources(), true ) ) {
		return new WP_Error( 'invalid_target', __( 'Der angeforderte OAuth-Endpunkt ist nicht erlaubt.', 'wp-agent-companion' ) );
	}
	$scope = wpagent_companion_oauth_normalize_scope( $params['scope'] ?? null );
	if ( is_wp_error( $scope ) ) {
		return $scope;
	}
	if ( wpagent_companion_oauth_scope_contains( $scope, 'offline_access' ) && ! wpagent_companion_oauth_client_allows_grant( $client, 'refresh_token' ) ) {
		return new WP_Error( 'invalid_scope', __( 'Dieser OAuth-Client darf keine dauerhafte Verbindung anfordern.', 'wp-agent-companion' ) );
	}
	return array(
		'client_id'   => $client_id,
		'client_name' => (string) ( $client['client_name'] ?? 'MCP client' ),
		'resource'    => $resource,
		'scope'       => $scope,
	);
}

/** @return array{hash:string,record:array<string,mixed>}|null */
function wpagent_companion_oauth_device_record_by_user_code( string $user_code, ?array $records = null ) {
	$hash    = wpagent_companion_oauth_hash( $user_code );
	$records = null === $records ? wpagent_companion_oauth_device_codes() : $records;
	foreach ( $records as $device_hash => $record ) {
		if ( is_array( $record ) && hash_equals( (string) ( $record['user_code_hash'] ?? '' ), $hash ) ) {
			return array( 'hash' => (string) $device_hash, 'record' => $record );
		}
	}
	return null;
}

function wpagent_companion_oauth_device_authorization( WP_REST_Request $request ): WP_REST_Response {
	if ( ! wpagent_companion_gate_open() ) {
		return wpagent_companion_oauth_error_response( 'temporarily_unavailable', __( 'Die wpagently-Verbindung ist auf dieser Website geschlossen.', 'wp-agent-companion' ), 503 );
	}
	if ( ! is_ssl() && 'local' !== wp_get_environment_type() ) {
		return wpagent_companion_oauth_error_response( 'invalid_request', __( 'OAuth benötigt HTTPS.', 'wp-agent-companion' ) );
	}
	$lock = wpagent_companion_oauth_acquire_lock();
	if ( is_wp_error( $lock ) ) {
		return wpagent_companion_oauth_error_response( $lock->get_error_code(), $lock->get_error_message(), (int) ( $lock->get_error_data()['status'] ?? 503 ) );
	}
	try {
		// Auch ungültige Requests verbrauchen einen Slot. Der öffentlich erreichbare
		// Endpoint kann den begrenzten Pending-Pool daher nicht durch billige
		// Vorvalidierung oder parallele Requests erschöpfen.
		$allowed = wpagent_companion_oauth_consume_device_authorization_slot_locked();
		if ( is_wp_error( $allowed ) ) {
			return wpagent_companion_oauth_error_response( $allowed->get_error_code(), $allowed->get_error_message(), (int) ( $allowed->get_error_data()['status'] ?? 429 ) );
		}
		$validated = wpagent_companion_oauth_validate_device_authorization_request( $request->get_params() );
		if ( is_wp_error( $validated ) ) {
			return wpagent_companion_oauth_error_response( $validated->get_error_code(), $validated->get_error_message() );
		}
		$pruned = wpagent_companion_oauth_prune_locked();
		if ( is_wp_error( $pruned ) ) {
			$pruned = wpagent_companion_oauth_public_storage_error( $pruned );
			return wpagent_companion_oauth_error_response( $pruned->get_error_code(), $pruned->get_error_message(), (int) ( $pruned->get_error_data()['status'] ?? 500 ) );
		}
		$records = wpagent_companion_oauth_device_codes();
		$active  = 0;
		foreach ( $records as $record ) {
			if ( is_array( $record ) && absint( $record['expires_at'] ?? 0 ) > time() ) {
				$active++;
			}
		}
		if ( $active >= wpagent_companion_oauth_limit() ) {
			return wpagent_companion_oauth_error_response( 'temporarily_unavailable', __( 'Zu viele offene OAuth-Anmeldungen.', 'wp-agent-companion' ), 503 );
		}
		$device_code = '';
		for ( $attempt = 0; $attempt < 5; $attempt++ ) {
			$device_code = 'wpdc_' . wpagent_companion_oauth_random();
			$user_code   = wpagent_companion_oauth_new_device_user_code();
			$device_hash = wpagent_companion_oauth_hash( $device_code );
			$user_hash   = wpagent_companion_oauth_hash( $user_code );
			if ( ! isset( $records[ $device_hash ] ) && ! wpagent_companion_oauth_device_record_by_user_code( $user_code, $records ) ) {
				break;
			}
			$device_code = '';
		}
		if ( '' === $device_code ) {
			return wpagent_companion_oauth_error_response( 'temporarily_unavailable', __( 'Der Gerätecode konnte nicht sicher erzeugt werden. Versuche es erneut.', 'wp-agent-companion' ), 503 );
		}
		$now                     = time();
		$records[ $device_hash ] = array_merge(
			$validated,
			array(
				'user_code_hash' => $user_hash,
				'status'         => 'pending',
				'user_id'        => 0,
				'interval'       => WPAGENT_COMPANION_OAUTH_DEVICE_INTERVAL,
				'last_polled_at' => 0,
				'created_at'     => $now,
				'expires_at'     => $now + WPAGENT_COMPANION_OAUTH_DEVICE_CODE_TTL,
			)
		);
		$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $records );
		if ( is_wp_error( $stored ) ) {
			$stored = wpagent_companion_oauth_public_storage_error( $stored );
			return wpagent_companion_oauth_error_response( $stored->get_error_code(), $stored->get_error_message(), (int) ( $stored->get_error_data()['status'] ?? 500 ) );
		}
		$verification_uri = wpagent_companion_oauth_device_verification_uri();
		$response = new WP_REST_Response(
			array(
				'device_code'               => $device_code,
				'user_code'                 => $user_code,
				'verification_uri'          => $verification_uri,
				'verification_uri_complete' => add_query_arg( 'user_code', rawurlencode( $user_code ), $verification_uri ),
				'expires_in'                => WPAGENT_COMPANION_OAUTH_DEVICE_CODE_TTL,
				'interval'                  => WPAGENT_COMPANION_OAUTH_DEVICE_INTERVAL,
			),
			200
		);
		$response->header( 'Cache-Control', 'no-store' );
		$response->header( 'Pragma', 'no-cache' );
		return $response;
	} finally {
		wpagent_companion_oauth_release_lock( $lock );
	}
}

/** @return array{approved:bool}|WP_Error */
function wpagent_companion_oauth_decide_device_code( $raw_user_code, int $user_id, bool $approved ) {
	$user_code = wpagent_companion_oauth_normalize_device_user_code( $raw_user_code );
	if ( is_wp_error( $user_code ) ) {
		return $user_code;
	}
	$lock = wpagent_companion_oauth_acquire_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$pruned = wpagent_companion_oauth_prune_locked();
		if ( is_wp_error( $pruned ) ) {
			return $pruned;
		}
		$records = wpagent_companion_oauth_device_codes();
		$found   = wpagent_companion_oauth_device_record_by_user_code( $user_code, $records );
		if ( ! is_array( $found ) || absint( $found['record']['expires_at'] ?? 0 ) <= time() || 'pending' !== (string) ( $found['record']['status'] ?? '' ) ) {
			return new WP_Error( 'invalid_user_code', __( 'Der Gerätecode ist ungültig oder abgelaufen.', 'wp-agent-companion' ) );
		}
		if ( $approved ) {
			$user = get_user_by( 'id', $user_id );
			if ( ! $user || ! wpagent_companion_is_safe_connection_user( $user ) || ! wpagent_companion_gate_open() ) {
				return new WP_Error( 'access_denied', __( 'Der gewählte Agent-Nutzer ist nicht freigegeben.', 'wp-agent-companion' ) );
			}
		}
		$records[ $found['hash'] ]['status']     = $approved ? 'approved' : 'denied';
		$records[ $found['hash'] ]['user_id']    = $approved ? $user_id : 0;
		$records[ $found['hash'] ]['decided_at'] = time();
		$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $records );
		if ( is_wp_error( $stored ) ) {
			return $stored;
		}
		return array( 'approved' => $approved );
	} finally {
		wpagent_companion_oauth_release_lock( $lock );
	}
}

/** @return WP_Error */
function wpagent_companion_oauth_device_exchange_failure( WP_Error $error, array $devices, array $grants, array $tokens ): WP_Error {
	$failed = false;
	foreach (
		array(
			WPAGENT_COMPANION_OPT_OAUTH_TOKENS       => $tokens,
			WPAGENT_COMPANION_OPT_OAUTH_GRANTS       => $grants,
			WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES => $devices,
		) as $option => $snapshot
	) {
		if ( is_wp_error( wpagent_companion_oauth_persist( $option, $snapshot ) ) ) {
			$failed = true;
		}
	}
	return $failed
		? new WP_Error( 'server_error', __( 'Der OAuth-Status konnte nicht sicher gespeichert werden.', 'wp-agent-companion' ), array( 'status' => 500 ) )
		: $error;
}

/** @return array<string,mixed>|WP_Error */
function wpagent_companion_oauth_exchange_device_code( array $params ) {
	$raw_device_code = is_string( $params['device_code'] ?? null ) ? $params['device_code'] : '';
	$client_id       = is_string( $params['client_id'] ?? null ) && strlen( $params['client_id'] ) <= 512 ? $params['client_id'] : '';
	$resource        = is_string( $params['resource'] ?? null ) && strlen( $params['resource'] ) <= 2048 ? untrailingslashit( $params['resource'] ) : '';
	$resource_too_long = is_string( $params['resource'] ?? null ) && strlen( $params['resource'] ) > 2048;
	if ( ! preg_match( '/^wpdc_[A-Za-z0-9_-]{32,256}$/', $raw_device_code ) || '' === $client_id || $resource_too_long ) {
		return new WP_Error( 'invalid_grant', __( 'Der Gerätecode ist ungültig oder abgelaufen.', 'wp-agent-companion' ) );
	}
	$lock = wpagent_companion_oauth_acquire_lock();
	if ( is_wp_error( $lock ) ) {
		return $lock;
	}
	try {
		$pruned = wpagent_companion_oauth_prune_locked();
		if ( is_wp_error( $pruned ) ) {
			return $pruned;
		}
		$devices = wpagent_companion_oauth_device_codes();
		$hash    = wpagent_companion_oauth_hash( $raw_device_code );
		$record  = $devices[ $hash ] ?? null;
		if ( ! is_array( $record ) ) {
			return new WP_Error( 'invalid_grant', __( 'Der Gerätecode ist ungültig oder abgelaufen.', 'wp-agent-companion' ) );
		}
		$client = wpagent_companion_oauth_clients()[ $client_id ] ?? null;
		if (
			! is_array( $client )
			|| '' !== (string) ( $client['revoked_at'] ?? '' )
			|| ! wpagent_companion_oauth_client_allows_grant( $client, WPAGENT_COMPANION_OAUTH_DEVICE_GRANT )
			|| ( wpagent_companion_oauth_scope_contains( (string) ( $record['scope'] ?? '' ), 'offline_access' ) && ! wpagent_companion_oauth_client_allows_grant( $client, 'refresh_token' ) )
			|| ! hash_equals( (string) ( $record['client_id'] ?? '' ), $client_id )
			|| ( '' !== $resource && ! hash_equals( (string) ( $record['resource'] ?? '' ), $resource ) )
		) {
			return new WP_Error( 'invalid_grant', __( 'Der Gerätecode passt nicht zu diesem Client.', 'wp-agent-companion' ) );
		}
		if ( array_key_exists( 'scope', $params ) ) {
			$scope = wpagent_companion_oauth_normalize_scope( $params['scope'] );
			if ( is_wp_error( $scope ) || ! hash_equals( (string) $record['scope'], (string) $scope ) ) {
				return new WP_Error( 'invalid_grant', __( 'Der Gerätecode passt nicht zu diesem Client.', 'wp-agent-companion' ) );
			}
		}
		$now = time();
		if ( absint( $record['expires_at'] ?? 0 ) <= $now ) {
			unset( $devices[ $hash ] );
			$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $devices );
			return is_wp_error( $stored ) ? $stored : new WP_Error( 'expired_token', __( 'Der Gerätecode ist abgelaufen.', 'wp-agent-companion' ) );
		}
		$status = (string) ( $record['status'] ?? 'pending' );
		if ( 'pending' === $status ) {
			$interval = max( WPAGENT_COMPANION_OAUTH_DEVICE_INTERVAL, min( WPAGENT_COMPANION_OAUTH_DEVICE_MAX_INTERVAL, absint( $record['interval'] ?? WPAGENT_COMPANION_OAUTH_DEVICE_INTERVAL ) ) );
			if ( absint( $record['last_polled_at'] ?? 0 ) > 0 && $now < absint( $record['last_polled_at'] ) + $interval ) {
				$devices[ $hash ]['interval']       = min( WPAGENT_COMPANION_OAUTH_DEVICE_MAX_INTERVAL, $interval + WPAGENT_COMPANION_OAUTH_DEVICE_INTERVAL );
				$devices[ $hash ]['last_polled_at'] = $now;
				$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $devices );
				return is_wp_error( $stored ) ? $stored : new WP_Error( 'slow_down', __( 'Die Geräteabfrage erfolgt zu schnell.', 'wp-agent-companion' ) );
			}
			$devices[ $hash ]['last_polled_at'] = $now;
			$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $devices );
			return is_wp_error( $stored ) ? $stored : new WP_Error( 'authorization_pending', __( 'Die Gerätefreigabe steht noch aus.', 'wp-agent-companion' ) );
		}
		if ( 'denied' === $status ) {
			unset( $devices[ $hash ] );
			$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $devices );
			return is_wp_error( $stored ) ? $stored : new WP_Error( 'access_denied', __( 'Die Gerätefreigabe wurde abgelehnt.', 'wp-agent-companion' ) );
		}
		if ( 'approved' !== $status ) {
			return new WP_Error( 'invalid_grant', __( 'Der Gerätecode ist ungültig oder abgelaufen.', 'wp-agent-companion' ) );
		}
		$user = get_user_by( 'id', absint( $record['user_id'] ?? 0 ) );
		if ( ! $user || ! wpagent_companion_is_safe_connection_user( $user ) || ! wpagent_companion_gate_open() ) {
			unset( $devices[ $hash ] );
			$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $devices );
			return is_wp_error( $stored ) ? $stored : new WP_Error( 'access_denied', __( 'Der freigegebene Agent-Nutzer ist nicht mehr verfügbar.', 'wp-agent-companion' ) );
		}
		$original_devices = $devices;
		$original_grants  = wpagent_companion_oauth_grants();
		$original_tokens  = wpagent_companion_oauth_tokens();
		$grants           = wpagent_companion_oauth_make_grant_room_locked( $original_grants, wpagent_companion_oauth_limit() );
		if ( count( $grants ) >= wpagent_companion_oauth_limit() ) {
			return new WP_Error( 'temporarily_unavailable', __( 'Zu viele aktive OAuth-Verbindungen. Widerrufe zuerst eine nicht mehr benötigte Verbindung.', 'wp-agent-companion' ) );
		}
		$grant_id = wp_generate_uuid4();
		$grant    = array(
			'grant_id' => $grant_id,
			'client_id' => $client_id,
			'user_id' => (int) $user->ID,
			'scope' => (string) $record['scope'],
			'resource' => (string) $record['resource'],
			'created_at' => gmdate( 'c' ),
			'revoked_at' => '',
		);
		$pair = wpagent_companion_oauth_new_pair_locked( $grant, wp_generate_uuid4(), $original_tokens );
		if ( is_wp_error( $pair ) ) {
			return wpagent_companion_oauth_device_exchange_failure( $pair, $original_devices, $original_grants, $original_tokens );
		}
		$grants[ $grant_id ] = $grant;
		$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_GRANTS, $grants );
		if ( is_wp_error( $stored ) ) {
			return wpagent_companion_oauth_device_exchange_failure( $stored, $original_devices, $original_grants, $original_tokens );
		}
		unset( $devices[ $hash ] );
		$stored = wpagent_companion_oauth_persist( WPAGENT_COMPANION_OPT_OAUTH_DEVICE_CODES, $devices );
		if ( is_wp_error( $stored ) ) {
			return wpagent_companion_oauth_device_exchange_failure( $stored, $original_devices, $original_grants, $original_tokens );
		}
		return $pair;
	} finally {
		wpagent_companion_oauth_release_lock( $lock );
	}
}
