/**
 * Copyright © 2023-2024 Nevis Security AG. All rights reserved.
 */

import { PlatformOperation } from './PlatformOperation';
import { Aaid } from '../../localData/Aaid';
import { Authenticator } from '../../localData/Authenticator';
import NevisMobileAuthenticationSdkReact from '../../MobileAuthenticationSdk';
import { PasswordValidatedMessage } from '../../model/messages/out/PasswordValidatedMessage';
import { PinValidatedMessage } from '../../model/messages/out/PinValidatedMessage';
import type { PasswordChangeContext } from '../../operations/password/PasswordChangeContext';
import {
	PasswordChangeHandler,
	PasswordChangeHandlerImpl,
} from '../../operations/password/PasswordChangeHandler';
import type { PasswordChanger } from '../../operations/password/PasswordChanger';
import type { PasswordEnroller } from '../../operations/password/PasswordEnroller';
import type { PasswordEnrollmentContext } from '../../operations/password/PasswordEnrollmentContext';
import {
	PasswordEnrollmentHandler,
	PasswordEnrollmentHandlerImpl,
} from '../../operations/password/PasswordEnrollmentHandler';
import { PinChangeContext } from '../../operations/pin/PinChangeContext';
import { PinChangeHandler, PinChangeHandlerImpl } from '../../operations/pin/PinChangeHandler';
import { PinChanger } from '../../operations/pin/PinChanger';
import { PinEnroller } from '../../operations/pin/PinEnroller';
import type { PinEnrollmentContext } from '../../operations/pin/PinEnrollmentContext';
import {
	PinEnrollmentHandler,
	PinEnrollmentHandlerImpl,
} from '../../operations/pin/PinEnrollmentHandler';
import type { AccountSelectionContext } from '../../operations/selection/AccountSelectionContext';
import {
	AccountSelectionHandler,
	AccountSelectionHandlerImpl,
} from '../../operations/selection/AccountSelectionHandler';
import { AccountSelector } from '../../operations/selection/AccountSelector';
import type { AuthenticatorSelectionContext } from '../../operations/selection/AuthenticatorSelectionContext';
import {
	AuthenticatorSelectionHandler,
	AuthenticatorSelectionHandlerImpl,
} from '../../operations/selection/AuthenticatorSelectionHandler';
import { AuthenticatorSelector } from '../../operations/selection/AuthenticatorSelector';
import type { BiometricUserVerificationHandler } from '../../operations/userverification/BiometricUserVerificationHandler';
import { BiometricUserVerifier } from '../../operations/userverification/BiometricUserVerifier';
import type { DevicePasscodeUserVerificationHandler } from '../../operations/userverification/DevicePasscodeUserVerificationHandler';
import { DevicePasscodeUserVerifier } from '../../operations/userverification/DevicePasscodeUserVerifier';
import type { FingerprintUserVerificationHandler } from '../../operations/userverification/FingerprintUserVerificationHandler';
import { FingerprintUserVerifier } from '../../operations/userverification/FingerprintUserVerifier';
import type { PasswordUserVerificationContext } from '../../operations/userverification/PasswordUserVerificationContext';
import type { PasswordUserVerificationHandler } from '../../operations/userverification/PasswordUserVerificationHandler';
import type { PasswordUserVerifier } from '../../operations/userverification/PasswordUserVerifier';
import type { PinUserVerificationContext } from '../../operations/userverification/PinUserVerificationContext';
import type { PinUserVerificationHandler } from '../../operations/userverification/PinUserVerificationHandler';
import { PinUserVerifier } from '../../operations/userverification/PinUserVerifier';
import type { UserVerificationContext } from '../../operations/userverification/UserVerificationContext';
import type { UserVerificationHandler } from '../../operations/userverification/UserVerificationHandler';

/**
 * Helps in following the states of user interaction operations during method
 * channel calls.
 */
export abstract class UserInteractionPlatformOperation extends PlatformOperation {
	/**
	 * The {@link AccountSelector} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract accountSelector?: AccountSelector;

	/**
	 * The {@link AuthenticatorSelector} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract authenticatorSelector?: AuthenticatorSelector;

	/**
	 * The {@link PinEnroller} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract pinEnroller?: PinEnroller;

	/**
	 * The {@link PasswordEnroller} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract passwordEnroller?: PasswordEnroller;

	/**
	 * The {@link PinChanger} given when an operation is started.
	 */
	abstract pinChanger?: PinChanger;

	/**
	 * The {@link PasswordChanger} given when an operation is started.
	 */
	abstract passwordChanger?: PasswordChanger;

	/**
	 * The {@link PinUserVerifier} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract pinUserVerifier?: PinUserVerifier;

	/**
	 * The {@link PasswordUserVerifier} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract passwordUserVerifier?: PasswordUserVerifier;

	/**
	 * The {@link BiometricUserVerifier} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract biometricUserVerifier?: BiometricUserVerifier;

	/**
	 * The {@link DevicePasscodeUserVerifier} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract devicePasscodeUserVerifier?: DevicePasscodeUserVerifier;

	/**
	 * The {@link FingerprintUserVerifier} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 */
	abstract fingerprintUserVerifier?: FingerprintUserVerifier;

	/**
	 * The {@link AccountSelectionHandler} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 * This is generated automatically based on the {@link operationId}.
	 */
	abstract accountSelectionHandler?: AccountSelectionHandler;

	/**
	 * The {@link AuthenticatorSelectionHandler} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 * This is generated automatically based on the {@link operationId}.
	 */
	abstract authenticatorSelectionHandler?: AuthenticatorSelectionHandler;

	/**
	 * The {@link PinEnrollmentHandler} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 * This is generated automatically based on the {@link operationId}.
	 */
	abstract pinEnrollmentHandler?: PinEnrollmentHandler;

	/**
	 * The {@link PasswordEnrollmentHandler} given when an operation is started.
	 *
	 * E.g.: During an out-of-band process.
	 * This is generated automatically based on the {@link operationId}.
	 */
	abstract passwordEnrollmentHandler?: PasswordEnrollmentHandler;

	/**
	 * The {@link PinChangeHandler} given when an operation is started.
	 *
	 * E.g.: During a pin change.
	 * This is generated automatically based on the {@link operationId}.
	 */
	abstract pinChangeHandler?: PinChangeHandler;

	/**
	 * The {@link PasswordChangeHandler} given when an operation is started.
	 *
	 * E.g.: During a password change.
	 * This is generated automatically based on the {@link operationId}.
	 */
	abstract passwordChangeHandler?: PasswordChangeHandler;

	/**
	 * The {@link UserVerificationHandler} given when an operation is in the state of user verification.
	 */
	abstract userVerificationHandler?: UserVerificationHandler;

	/**
	 * The account selection interaction.
	 *
	 * The implementing class must ask the user to choose one of the accounts
	 * exposed by the {@link AccountSelectionContext} and provide the choice to the
	 * {@link AccountSelectionHandler}.
	 *
	 * @param context the object containing the list of existing accounts and authenticators.
	 */
	selectAccount(context: AccountSelectionContext): void {
		this.accountSelector?.selectAccount(context, this.accountSelectionHandler!);
	}

	/**
	 * The authenticator selection interaction.
	 *
	 * The implementing class must ask the user to choose one of the authenticators
	 * exposed by the {@link AuthenticatorSelectionContext} and provide the choice to the
	 * {@link AuthenticatorSelectionHandler}.
	 *
	 * Note, that in the case of transaction confirmation (which can be considered
	 * a special case of authentication) the implementing classes must present
	 * the contents of the transaction (if any) to the user for verification
	 * @see {@link AuthenticatorSelectionContext.transactionConfirmationData}
	 *
	 * @param context the object containing the list of existing authenticators.
	 */
	selectAuthenticator(context: AuthenticatorSelectionContext): void {
		this.authenticatorSelector?.selectAuthenticator(
			context,
			this.authenticatorSelectionHandler!
		);
	}

	/**
	 * The method that will be invoked till either the user provides a PIN that
	 * conforms with the format specified by the {@link PinPolicy} or till the
	 * operation is cancelled (through the {@link PinEnrollmentHandler.cancel}).
	 *
	 * @param context the context.
	 */
	enrollPin(context: PinEnrollmentContext): void {
		this.pinEnroller?.enrollPin(context, this.pinEnrollmentHandler!);
	}

	/**
	 * The method that will be invoked till either the user provides a password that
	 * conforms with the format specified by the {@link PasswordPolicy} or till the
	 * operation is cancelled (through the {@link PasswordEnrollmentHandler.cancel}).
	 *
	 * @param context the context.
	 */
	enrollPassword(context: PasswordEnrollmentContext): void {
		this.passwordEnroller?.enrollPassword(context, this.passwordEnrollmentHandler!);
	}

	/**
	 * The method that will be invoked till either the user provides the old PIN
	 * and a new PIN that conforms with the format specified by the {@link PinPolicy},
	 * or till the operation is cancelled (through the {@link PinChangeHandler.cancel}),
	 * or till the PIN authenticator is permanently locked because the user provided
	 * too many times an invalid PIN.
	 *
	 * @param context the context.
	 */
	changePin(context: PinChangeContext) {
		this.pinChanger?.changePin(context, this.pinChangeHandler!);
	}

	/**
	 * The method that will be invoked till either the user provides the old password
	 * and a new password that conforms with the format specified by the {@link PasswordPolicy},
	 * or till the operation is cancelled (through the {@link PasswordChangeHandler.cancel}),
	 * or till the password authenticator is permanently locked because the user provided
	 * too many times an invalid password.
	 *
	 * @param context the context.
	 */
	changePassword(context: PasswordChangeContext) {
		this.passwordChanger?.changePassword(context, this.passwordChangeHandler!);
	}

	/**
	 * The user verification interaction.
	 *
	 * In the case of the registration the user must provide credentials again as
	 * required by the FIDO UAF protocol.
	 * In the case of the authentication, this is invoked for the user to provide
	 * credentials.
	 *
	 * If the user provided invalid credentials, and it results in a non-recoverable
	 * error, then `onSuccess` method will be invoked.
	 *
	 * @param context the object providing the information required for the verification
	 * process.
	 * @param handler the object that must be notified with the result of the interaction.
	 */
	verifyUser(context: UserVerificationContext, handler: UserVerificationHandler): Promise<void> {
		switch (context.authenticator.aaid) {
			case Aaid.PIN.rawValue():
				return this.pinUserVerifier!.verifyPin(
					context as PinUserVerificationContext,
					handler as PinUserVerificationHandler
				);
			case Aaid.PASSWORD.rawValue():
				return this.passwordUserVerifier!.verifyPassword(
					context as PasswordUserVerificationContext,
					handler as PasswordUserVerificationHandler
				);
			case Aaid.BIOMETRIC.rawValue():
				return this.biometricUserVerifier!.verifyBiometric(
					context,
					handler as BiometricUserVerificationHandler
				);
			case Aaid.DEVICE_PASSCODE.rawValue():
				return this.devicePasscodeUserVerifier!.verifyDevicePasscode(
					context,
					handler as DevicePasscodeUserVerificationHandler
				);
			case Aaid.FINGERPRINT.rawValue():
				return this.fingerprintUserVerifier!.verifyFingerprint(
					context,
					handler as FingerprintUserVerificationHandler
				);
		}

		return Promise.reject(
			new Error(
				`No verifier found for Authenticator aaid ${context.authenticator.aaid} when verifying the user.`
			)
		);
	}

	/**
	 * This method is invoked when either valid local system credentials (biometric,
	 * fingerprint) or valid SDK-managed credentials (PIN, password) were provided
	 * and verified locally.
	 *
	 * This method can be used for instance to display some progress message
	 * indicating that the operation is ongoing.
	 *
	 * Note that invoking this method does not mean that the UAF operation completed
	 * successfully (this is notified through `onSuccess` methods once the FIDO UAF
	 * server validates the request generated with the credentials).
	 *
	 * @param authenticator the object describing the authenticator where credentials
	 * were validated.
	 */
	onValidCredentialsProvided(authenticator: Authenticator): void {
		switch (authenticator.aaid) {
			case Aaid.PIN.rawValue():
				return this.pinUserVerifier?.onValidCredentialsProvided();
			case Aaid.PASSWORD.rawValue():
				return this.passwordUserVerifier?.onValidCredentialsProvided();
			case Aaid.BIOMETRIC.rawValue():
				return this.biometricUserVerifier?.onValidCredentialsProvided();
			case Aaid.DEVICE_PASSCODE.rawValue():
				return this.devicePasscodeUserVerifier?.onValidCredentialsProvided();
			case Aaid.FINGERPRINT.rawValue():
				return this.fingerprintUserVerifier?.onValidCredentialsProvided();
		}
		throw new Error(
			`No verifier found for Authenticator aaid ${authenticator.aaid} when valid credentials provided.`
		);
	}

	/**
	 * Performs validation other than the minimum and maximum PIN length during PIN enrollment.
	 *
	 * @param pin the PIN to be validated.
	 */
	validatePinForEnrollment(pin: string) {
		this.pinEnroller?.pinPolicy.validatePinForEnrollment(
			pin,
			() => {
				(async () => {
					const message = new PinValidatedMessage(this.operationId, undefined, undefined);
					await NevisMobileAuthenticationSdkReact.pinValidatedForEnrollment(message);
				})();
			},
			(error) => {
				(async () => {
					const message = new PinValidatedMessage(
						this.operationId,
						error.description,
						error.cause
					);
					await NevisMobileAuthenticationSdkReact.pinValidatedForEnrollment(message);
				})();
			}
		);
	}

	/**
	 * Performs validation during password enrollment.
	 *
	 * @param password the password to be validated.
	 */
	validatePasswordForEnrollment(password: string) {
		this.passwordEnroller?.passwordPolicy.validatePasswordForEnrollment(
			password,
			() => {
				(async () => {
					const message = new PasswordValidatedMessage(
						this.operationId,
						undefined,
						undefined
					);
					await NevisMobileAuthenticationSdkReact.passwordValidatedForEnrollment(message);
				})();
			},
			(error) => {
				(async () => {
					const message = new PasswordValidatedMessage(
						this.operationId,
						error.description,
						error.cause
					);
					await NevisMobileAuthenticationSdkReact.passwordValidatedForEnrollment(message);
				})();
			}
		);
	}

	/**
	 * Performs validation other than the minimum and maximum PIN length during PIN change.
	 *
	 * @param pin the PIN to be validated.
	 */
	validatePinForPinChange(pin: string) {
		this.pinChanger?.pinPolicy.validatePinForPinChange(
			pin,
			() => {
				(async () => {
					const message = new PinValidatedMessage(this.operationId, undefined, undefined);
					await NevisMobileAuthenticationSdkReact.pinValidatedForPinChange(message);
				})();
			},
			(error) => {
				(async () => {
					const message = new PinValidatedMessage(
						this.operationId,
						error.description,
						error.cause
					);
					await NevisMobileAuthenticationSdkReact.pinValidatedForPinChange(message);
				})();
			}
		);
	}

	/**
	 * Performs validation during password enrollment.
	 *
	 * @param password the password to be validated.
	 */
	validatePasswordForPasswordChange(password: string) {
		this.passwordChanger?.passwordPolicy.validatePasswordForPasswordChange(
			password,
			() => {
				(async () => {
					const message = new PasswordValidatedMessage(
						this.operationId,
						undefined,
						undefined
					);
					await NevisMobileAuthenticationSdkReact.passwordValidatedForPasswordChange(
						message
					);
				})();
			},
			(error) => {
				(async () => {
					const message = new PasswordValidatedMessage(
						this.operationId,
						error.description,
						error.cause
					);
					await NevisMobileAuthenticationSdkReact.passwordValidatedForPasswordChange(
						message
					);
				})();
			}
		);
	}
}

export class UserInteractionPlatformOperationImpl extends UserInteractionPlatformOperation {
	operationId: string;
	accountSelector?: AccountSelector;
	accountSelectionHandler?: AccountSelectionHandler;
	authenticatorSelector?: AuthenticatorSelector;
	authenticatorSelectionHandler?: AuthenticatorSelectionHandler;
	pinEnroller?: PinEnroller;
	pinEnrollmentHandler?: PinEnrollmentHandler;
	passwordEnroller?: PasswordEnroller;
	passwordEnrollmentHandler?: PasswordEnrollmentHandler;
	pinChanger?: PinChanger;
	pinChangeHandler?: PinChangeHandler;
	passwordChanger?: PasswordChanger;
	passwordChangeHandler?: PasswordChangeHandler;
	pinUserVerifier?: PinUserVerifier;
	passwordUserVerifier?: PasswordUserVerifier;
	biometricUserVerifier?: BiometricUserVerifier;
	devicePasscodeUserVerifier?: DevicePasscodeUserVerifier;
	fingerprintUserVerifier?: FingerprintUserVerifier;
	userVerificationHandler?: UserVerificationHandler;

	constructor(
		operationId: string,
		accountSelector?: AccountSelector,
		authenticatorSelector?: AuthenticatorSelector,
		pinEnroller?: PinEnroller,
		passwordEnroller?: PasswordEnroller,
		pinChanger?: PinChanger,
		passwordChanger?: PasswordChanger,
		pinUserVerifier?: PinUserVerifier,
		passwordUserVerifier?: PasswordUserVerifier,
		biometricUserVerifier?: BiometricUserVerifier,
		devicePasscodeUserVerifier?: DevicePasscodeUserVerifier,
		fingerprintUserVerifier?: FingerprintUserVerifier
	) {
		super();
		this.operationId = operationId;
		this.accountSelector = accountSelector;
		this.authenticatorSelector = authenticatorSelector;
		this.pinEnroller = pinEnroller;
		this.passwordEnroller = passwordEnroller;
		this.pinChanger = pinChanger;
		this.passwordChanger = passwordChanger;
		this.pinUserVerifier = pinUserVerifier;
		this.passwordUserVerifier = passwordUserVerifier;
		this.biometricUserVerifier = biometricUserVerifier;
		this.devicePasscodeUserVerifier = devicePasscodeUserVerifier;
		this.fingerprintUserVerifier = fingerprintUserVerifier;
		this.accountSelectionHandler = new AccountSelectionHandlerImpl(operationId);
		this.authenticatorSelectionHandler = new AuthenticatorSelectionHandlerImpl(operationId);
		this.pinEnrollmentHandler = new PinEnrollmentHandlerImpl(operationId);
		this.passwordEnrollmentHandler = new PasswordEnrollmentHandlerImpl(operationId);
		this.pinChangeHandler = new PinChangeHandlerImpl(operationId);
		this.passwordChangeHandler = new PasswordChangeHandlerImpl(operationId);
	}
}
