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

import { UserInteractionPlatformOperation } from '../../cache/operation/UserInteractionPlatformOperation';
import { PlatformOperationCache } from '../../cache/PlatformOperationCache';
import NevisMobileAuthenticationSdkReact from '../../MobileAuthenticationSdk';
import { OperationIdMessage } from '../../model/messages/out/OperationIdMessage';

/**
 * An object that can be used to pause or resume listening for OS credentials (i.e. fingerprint, face
 * recognition) and to cancel the whole operation while listening for credentials.
 *
 * **IMPORTANT** \
 * The {@link OsAuthenticationListenHandler} class is Android specific.
 *
 * This is used with {@link Aaid.BIOMETRIC}, {@link Aaid.DEVICE_PASSCODE} and {@link Aaid.FINGERPRINT}
 * authenticator attestation identifiers.
 *
 * @see
 * - {@link BiometricUserVerificationHandler.listenForOsCredentials}
 * - {@link DevicePasscodeUserVerificationHandler.listenForOsCredentials}
 * - {@link FingerprintUserVerificationHandler.listenForOsCredentials}
 */
export abstract class OsAuthenticationListenHandler {
	/**
	 * Cancels the authentication operation.
	 *
	 * This will result in the operation being canceled and an {@link OperationError} or
	 * an {@link OperationFidoError} with a {@link FidoErrorCodeType.UserCanceled} will be returned.
	 */
	abstract cancelAuthentication(): Promise<void>;

	/**
	 * Pauses listening for OS credentials.
	 *
	 * If the application is listening for OS credentials, and it is brought to the background, then
	 * the operating system will cancel automatically listening for credentials and will send an error.
	 * This method must be invoked when the application is brought to the background before the OS
	 * cancels the authentication and the SDK sends an error.
	 *
	 * Invoking this method will have effect only if {@link cancelAuthentication} was not previously
	 * invoked.
	 *
	 * The method can be invoked from the [AppState](https://reactnative.dev/docs/appstate) event listener
	 * when the new state is `inactive` or `background` and the current state is `active`.
	 * Note that this approach does not work in some devices (in these devices the event listener is
	 * invoked after the OS cancels the authentication).
	 *
	 * For example:
	 * ```ts
	 *    const appState = useRef(AppState.currentState);
	 *    const [listenHandler, setListenHandler] = useState<OsAuthenticationListenHandler>();
	 *
	 *    useCallback(() => {
	 *        const onStateChange = async (nextAppState: AppStateStatus) => {
	 *            if (appState.current === 'active' && nextAppState.match(/inactive|background/)) {
	 *                if (listenHandler !== undefined) {
	 *                    await listenHandler.pauseListening()
	 *                       .then((newListenHandler) => {
	 *                          setListenHandler(newListenHandler);
	 *                      })
	 *                      .catch(ErrorHandler.handle.bind(null, OperationType.unknown));
	 *                }
	 *            }
	 *
	 *            appState.current = nextAppState;
	 *        };
	 *
	 *        const subscription = AppState.addEventListener('change', onStateChange);
	 *        return () => subscription.remove();
	 *    }, [appState, listenHandler]);
	 * ```
	 *
	 * @returns the {@link OsAuthenticationListenHandler} to handle the new listening.
	 */
	abstract pauseListening(): Promise<OsAuthenticationListenHandler>;

	/**
	 * Resumes listening for OS credentials.
	 *
	 * If the application is listening for OS credentials and it is brought to the background, then
	 * the operating system will cancel automatically listening for credentials.
	 * This method must be invoked when the application is brought to the foreground again to resume
	 * listening for credentials.
	 *
	 * Invoking this method will have effect only if {@link cancelAuthentication} was not previously
	 * invoked.
	 *
	 * The method is typically invoked from the [AppState](https://reactnative.dev/docs/appstate) event
	 * listener when the new state is `active` and the current state is `inactive` or `background`.
	 *
	 * For example:
	 * ```ts
	 *    const appState = useRef(AppState.currentState);
	 *    const [listenHandler, setListenHandler] = useState<OsAuthenticationListenHandler>();
	 *
	 *    useCallback(() => {
	 *        const onStateChange = async (nextAppState: AppStateStatus) => {
	 *            if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
	 *                if (listenHandler !== undefined) {
	 *                    await listenHandler.resumeListening()
	 *                       .then((newListenHandler) => {
	 *                          setListenHandler(newListenHandler);
	 *                      })
	 *                      .catch(ErrorHandler.handle.bind(null, OperationType.unknown));
	 *                }
	 *            }
	 *
	 *            appState.current = nextAppState;
	 *        };
	 *
	 *        const subscription = AppState.addEventListener('change', onStateChange);
	 *        return () => subscription.remove();
	 *    }, [appState, listenHandler]);
	 * ```
	 *
	 * @returns the {@link OsAuthenticationListenHandler} to handle the new listening.
	 */
	abstract resumeListening(): Promise<OsAuthenticationListenHandler>;
}

export class OsAuthenticationListenHandlerImpl extends OsAuthenticationListenHandler {
	private readonly _operationId: string;

	constructor(operationId: string) {
		super();
		this._operationId = operationId;
	}

	async cancelAuthentication(): Promise<void> {
		const message = new OperationIdMessage(this._operationId);
		return NevisMobileAuthenticationSdkReact.cancelAuthentication(message);
	}

	async pauseListening(): Promise<OsAuthenticationListenHandler> {
		const operation = PlatformOperationCache.getInstance().read(this._operationId);
		if (
			!(operation instanceof UserInteractionPlatformOperation) ||
			operation.userVerificationHandler === undefined
		) {
			return this;
		}

		const message = new OperationIdMessage(this._operationId);
		return NevisMobileAuthenticationSdkReact.pauseListening(message).then(() => {
			return this;
		});
	}

	async resumeListening(): Promise<OsAuthenticationListenHandler> {
		const operation = PlatformOperationCache.getInstance().read(this._operationId);
		if (
			!(operation instanceof UserInteractionPlatformOperation) ||
			operation.userVerificationHandler === undefined
		) {
			return this;
		}

		const message = new OperationIdMessage(this._operationId);
		return NevisMobileAuthenticationSdkReact.resumeListening(message).then(() => {
			return this;
		});
	}
}
