<?php
/**
 * Bootstrap e verifica crittografica WebAuthn tramite web-auth/webauthn-framework.
 *
 * Incapsula tutta l'interazione con la libreria, così il resto del plugin
 * lavora solo con stringhe/array semplici.
 *
 * @package SPIDy_WP
 */

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

class SPIDy_WP_Webauthn_Service {

	/** @var array|null */
	private static $services = null;

	/**
	 * Costruisce (una sola volta per richiesta) i servizi della libreria.
	 */
	private static function services(): array {
		if ( null !== self::$services ) {
			return self::$services;
		}

		$attestationStatementSupportManager = \Webauthn\AttestationStatement\AttestationStatementSupportManager::create();
		$attestationStatementSupportManager->add( \Webauthn\AttestationStatement\NoneAttestationStatementSupport::create() );

		$serializerFactory = new \Webauthn\Denormalizer\WebauthnSerializerFactory( $attestationStatementSupportManager );
		$serializer        = $serializerFactory->create();

		$extensionOutputCheckerHandler = \Webauthn\AuthenticationExtensions\ExtensionOutputCheckerHandler::create();

		$algorithmManager = \Cose\Algorithm\Manager::create()->add(
			\Cose\Algorithm\Signature\ECDSA\ES256::create(),
			\Cose\Algorithm\Signature\RSA\RS256::create()
		);

		// A partire dalla 5.x della libreria, i validator non si costruiscono
		// più passando i singoli manager direttamente: vogliono un
		// CeremonyStepManager, assemblato tramite questa factory con gli
		// stessi identici componenti che costruivamo già sopra a mano.
		$ceremonyStepManagerFactory = new \Webauthn\CeremonyStep\CeremonyStepManagerFactory();
		$ceremonyStepManagerFactory->setAttestationStatementSupportManager( $attestationStatementSupportManager );
		$ceremonyStepManagerFactory->setAlgorithmManager( $algorithmManager );
		$ceremonyStepManagerFactory->setExtensionOutputCheckerHandler( $extensionOutputCheckerHandler );

		$attestationValidator = \Webauthn\AuthenticatorAttestationResponseValidator::create(
			$ceremonyStepManagerFactory->creationCeremony()
		);

		$assertionValidator = \Webauthn\AuthenticatorAssertionResponseValidator::create(
			$ceremonyStepManagerFactory->requestCeremony()
		);

		self::$services = array(
			'serializer'            => $serializer,
			'attestation_validator' => $attestationValidator,
			'assertion_validator'   => $assertionValidator,
		);

		return self::$services;
	}

	/**
	 * Converte base64url (formato usato dal browser/JS) in base64 standard
	 * con padding — formato atteso dal serializer della libreria.
	 */
	public static function base64url_to_base64( string $base64url ): string {
		$base64 = strtr( $base64url, '-_', '+/' );
		$pad    = strlen( $base64 ) % 4;
		if ( $pad > 0 ) {
			$base64 .= str_repeat( '=', 4 - $pad );
		}
		return $base64;
	}

	/**
	 * Verifica una risposta di REGISTRAZIONE (navigator.credentials.create()).
	 *
	 * @param array  $postData  Dati POST: credential_id, attestation_object, client_data_json (tutti base64url).
	 * @param string $challenge Challenge binaria generata dal server (stessa usata per le opzioni).
	 * @param int    $user_id   ID utente WordPress.
	 * @param string $rp_id     Dominio del sito (RP ID).
	 * @param string $rp_name   Nome leggibile del sito.
	 *
	 * @return array{source_json:string, credential_id:string, transports:array}|WP_Error
	 */
	public static function verify_registration( array $postData, string $challenge, int $user_id, string $rp_id, string $rp_name ) {
		try {
			$services = self::services();

			// I campi id/rawId/clientDataJSON/attestationObject nel formato
			// JSON della libreria vanno in base64url SENZA padding (è il
			// formato usato dalla spec WebAuthn stessa) — esattamente ciò
			// che il browser ci manda già. Convertirli in base64 standard
			// con padding (come si faceva prima) li rendeva illeggibili
			// al deserializzatore della libreria ("decodeNoPadding()
			// doesn't tolerate padding").
			$json = wp_json_encode(
				array(
					'id'       => $postData['credential_id'],
					'type'     => 'public-key',
					'rawId'    => $postData['credential_id'],
					'response' => array(
						'clientDataJSON'    => $postData['client_data_json'],
						'attestationObject' => $postData['attestation_object'],
					),
				)
			);

			$publicKeyCredential = $services['serializer']->deserialize( $json, \Webauthn\PublicKeyCredential::class, 'json' );

			if ( ! $publicKeyCredential->response instanceof \Webauthn\AuthenticatorAttestationResponse ) {
				return new WP_Error( 'spidy_wp_webauthn_wrong_type', __( 'Risposta del dispositivo non valida (tipo errato).', 'spidy-2fa-login' ) );
			}

			$userHandle = 'wp-user-' . $user_id;

			$rpEntity   = \Webauthn\PublicKeyCredentialRpEntity::create( $rp_name, $rp_id );
			$userEntity = \Webauthn\PublicKeyCredentialUserEntity::create( (string) $user_id, $userHandle, (string) $user_id );

			$creationOptions = \Webauthn\PublicKeyCredentialCreationOptions::create(
				$rpEntity,
				$userEntity,
				$challenge
			);

			$publicKeyCredentialSource = $services['attestation_validator']->check(
				$publicKeyCredential->response,
				$creationOptions,
				$rp_id
			);

			$sourceJson = $services['serializer']->serialize( $publicKeyCredentialSource, 'json' );

			return array(
				'source_json'   => $sourceJson,
				'credential_id' => $postData['credential_id'], // base64url, coerente col resto del plugin
				'transports'    => $publicKeyCredentialSource->transports,
			);
		} catch ( \Throwable $e ) {
			return new WP_Error( 'spidy_wp_webauthn_registration_failed', $e->getMessage() );
		}
	}

	/**
	 * Verifica una risposta di LOGIN (navigator.credentials.get()).
	 *
	 * @param array  $postData    Dati POST: credential_id, authenticator_data, client_data_json, signature (base64url).
	 * @param string $challenge   Challenge binaria generata dal server.
	 * @param string $sourceJson  JSON della PublicKeyCredentialSource salvata in registrazione.
	 * @param string $rp_id       Dominio del sito.
	 * @param array  $allowedIds  Elenco di Credential ID (base64url) ammessi per questo utente.
	 *
	 * @return array{source_json:string}|WP_Error
	 */
	public static function verify_assertion( array $postData, string $challenge, string $sourceJson, string $rp_id, array $allowedIds, int $user_id = 0 ) {
		try {
			$services = self::services();

			if ( ! in_array( $postData['credential_id'], $allowedIds, true ) ) {
				return new WP_Error( 'spidy_wp_webauthn_unknown_credential', __( 'Dispositivo non riconosciuto per questo utente.', 'spidy-2fa-login' ) );
			}

			$json = wp_json_encode(
				array(
					'id'       => $postData['credential_id'],
					'type'     => 'public-key',
					'rawId'    => $postData['credential_id'],
					'response' => array(
						'clientDataJSON'    => $postData['client_data_json'],
						'authenticatorData' => $postData['authenticator_data'],
						'signature'         => $postData['signature'],
						// Le passkey "discoverable" (es. Google Password
						// Manager su Android, Samsung Pass) restituiscono
						// sempre uno userHandle: va passato così com'è,
						// altrimenti la libreria rifiuta con "Invalid User
						// handle" perché non corrisponde a quello atteso
						// per la credenziale salvata in registrazione.
						'userHandle'        => ! empty( $postData['user_handle'] ) ? $postData['user_handle'] : null,
					),
				)
			);

			$publicKeyCredential = $services['serializer']->deserialize( $json, \Webauthn\PublicKeyCredential::class, 'json' );

			if ( ! $publicKeyCredential->response instanceof \Webauthn\AuthenticatorAssertionResponse ) {
				return new WP_Error( 'spidy_wp_webauthn_wrong_type', __( 'Risposta del dispositivo non valida (tipo errato).', 'spidy-2fa-login' ) );
			}

			$publicKeyCredentialSource = $services['serializer']->deserialize( $sourceJson, \Webauthn\PublicKeyCredentialSource::class, 'json' );

			SPIDy_WP_Login::debug_log(
				'verify_assertion — confronto user handle: '
				. 'salvato in registrazione (raw, base64url)=' . SPIDy_WP_Registration::base64url_encode( (string) $publicKeyCredentialSource->userHandle )
				. ' [' . strlen( (string) $publicKeyCredentialSource->userHandle ) . ' byte]'
				. ' | ricevuto dal browser ora (base64url così com\'è arrivato)=' . ( $postData['user_handle'] ?? '(vuoto/non inviato)' )
				. ' | ricevuto dal browser ora (raw decodificato, base64url)=' . ( ! empty( $postData['user_handle'] ) ? SPIDy_WP_Registration::base64url_encode( (string) $publicKeyCredential->response->userHandle ) : '(n/d)' )
				. ' [' . ( ! empty( $postData['user_handle'] ) && null !== $publicKeyCredential->response->userHandle ? strlen( (string) $publicKeyCredential->response->userHandle ) : 0 ) . ' byte]'
			);

			$requestOptions = \Webauthn\PublicKeyCredentialRequestOptions::create(
				$challenge,
				rpId: $rp_id,
				userVerification: \Webauthn\AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED
			);

			$updatedSource = $services['assertion_validator']->check(
				$publicKeyCredentialSource,
				$publicKeyCredential->response,
				$requestOptions,
				$rp_id,
				$user_id ? 'wp-user-' . $user_id : null
			);

			return array(
				'source_json' => $services['serializer']->serialize( $updatedSource, 'json' ),
			);
		} catch ( \Throwable $e ) {
			return new WP_Error( 'spidy_wp_webauthn_verification_failed', $e->getMessage() );
		}
	}
}
