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

import NevisMobileAuthentication

@objc(NevisMobileAuthenticationSdkReact)
public final class NevisMobileAuthenticationSdkReact: NSObject, Sendable {
	private(set) nonisolated(unsafe) var client: MobileAuthenticationClient?
	private let queue = DispatchQueue(withLabel: "ch.nevis.mobile.authentication.sdk.react")

	private let methodMessageMap: [ReactMethodName: ChannelInMessage.Type] = [
		.initClient: InitClientMessage.self,
		.payloadDecode: OutOfBandPayloadDecodeMessage.self,
		.oobOperation: OutOfBandOperationMessage.self,
		.oobRegister: OutOfBandRegistrationMessage.self,
		.oobAuthenticate: OutOfBandAuthenticationMessage.self,
		.register: RegistrationMessage.self,
		.authCloudApiRegister: AuthCloudApiRegistrationMessage.self,
		.authenticate: AuthenticationMessage.self,
		.deregister: DeregistrationMessage.self,
		.deviceInformationChange: DeviceInformationChangeMessage.self,
		.deviceInformationCheck: DeviceInformationCheckMessage.self,
		.deviceInformationSync: DeviceInformationSyncMessage.self,
		.pendingOutOfBandOperations: PendingOobOperationsMessage.self,
		.pinChange: PinChangeMessage.self,
		.passwordChange: PasswordChangeMessage.self,
		.localAccounts: LocalAccountsMessage.self,
		.localAuthenticators: LocalAuthenticatorsMessage.self,
		.localDeleteAuthenticator: LocalDeleteAuthenticatorMessage.self,
		.localDeviceInformation: LocalDeviceInformationMessage.self,
		.fidoUafAttestationInformation: FidoUafAttestationInformationMessage.self,
		.authenticatorAaid: AuthenticatorAaidMessage.self,
		.accountUsername: AccountUsernameMessage.self,
		.pinEnroll: PinEnrollMessage.self,
		.pinsChange: PinsChangeMessage.self,
		.pinValidatedForEnrollment: PinValidatedForEnrollmentMessage.self,
		.pinValidatedForPinChange: PinValidatedForPinChangeMessage.self,
		.pinVerify: PinVerifyMessage.self,
		.passwordEnroll: PasswordEnrollMessage.self,
		.passwordsChange: PasswordsChangeMessage.self,
		.passwordValidatedForEnrollment: PasswordValidatedForEnrollmentMessage.self,
		.passwordValidatedForPasswordChange: PasswordValidatedForPasswordChangeMessage.self,
		.passwordVerify: PasswordVerifyMessage.self,
		.cancel: CancelMessage.self,
		.isPolicyCompliant: IsPolicyCompliantMessage.self,
		.listenForOsCredentials: ListenForOsCredentialsMessage.self,
		.metaData: MetaDataMessage.self,
	]

	override init() { /* . */ }

	// MARK: - Basic SDK invocations

	@objc
	func initClient(_ message: [String: Any],
	                resolve: @escaping RCTPromiseResolveBlock,
	                reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .initClient), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func payloadDecode(_ message: [String: Any],
	                   resolve: @escaping RCTPromiseResolveBlock,
	                   reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .payloadDecode), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func oobOperation(_ message: [String: Any],
	                  resolve: @escaping RCTPromiseResolveBlock,
	                  reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .oobOperation), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func oobRegister(_ message: [String: Any],
	                 resolve: @escaping RCTPromiseResolveBlock,
	                 reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .oobRegister), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func oobAuthenticate(_ message: [String: Any],
	                     resolve: @escaping RCTPromiseResolveBlock,
	                     reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .oobAuthenticate), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func inBandRegister(_ message: [String: Any],
	                    resolve: @escaping RCTPromiseResolveBlock,
	                    reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .register), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func authCloudApiRegister(_ message: [String: Any],
	                          resolve: @escaping RCTPromiseResolveBlock,
	                          reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .authCloudApiRegister), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func authenticate(_ message: [String: Any],
	                  resolve: @escaping RCTPromiseResolveBlock,
	                  reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .authenticate), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func deregister(_ message: [String: Any],
	                resolve: @escaping RCTPromiseResolveBlock,
	                reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .deregister), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func deviceInformationChange(_ message: [String: Any],
	                             resolve: @escaping RCTPromiseResolveBlock,
	                             reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .deviceInformationChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func deviceInformationCheck(_ message: [String: Any],
	                            resolve: @escaping RCTPromiseResolveBlock,
	                            reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .deviceInformationCheck), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func deviceInformationSync(_ message: [String: Any],
	                           resolve: @escaping RCTPromiseResolveBlock,
	                           reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .deviceInformationSync), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pendingOutOfBandOperations(_ message: [String: Any],
	                                resolve: @escaping RCTPromiseResolveBlock,
	                                reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pendingOutOfBandOperations), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pinChange(_ message: [String: Any],
	               resolve: @escaping RCTPromiseResolveBlock,
	               reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pinChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func passwordChange(_ message: [String: Any],
	                    resolve: @escaping RCTPromiseResolveBlock,
	                    reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .passwordChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func localAccounts(_ message: [String: Any],
	                   resolve: @escaping RCTPromiseResolveBlock,
	                   reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .localAccounts), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func localAuthenticators(_ message: [String: Any],
	                         resolve: @escaping RCTPromiseResolveBlock,
	                         reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .localAuthenticators), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func localDeleteAuthenticator(_ message: [String: Any],
	                              resolve: @escaping RCTPromiseResolveBlock,
	                              reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .localDeleteAuthenticator), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func localDeviceInformation(_ message: [String: Any],
	                            resolve: @escaping RCTPromiseResolveBlock,
	                            reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .localDeviceInformation), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func fidoUafAttestationInformation(_ message: [String: Any],
	                                   resolve: @escaping RCTPromiseResolveBlock,
	                                   reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .fidoUafAttestationInformation), message: message, resolve: resolve, reject: reject)
	}

	// MARK: - Auxiliary SDK invocations

	@objc
	func authenticatorAaid(_ message: [String: Any],
	                       resolve: @escaping RCTPromiseResolveBlock,
	                       reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .authenticatorAaid), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pinEnroll(_ message: [String: Any],
	               resolve: @escaping RCTPromiseResolveBlock,
	               reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pinEnroll), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pinsChange(_ message: [String: Any],
	                resolve: @escaping RCTPromiseResolveBlock,
	                reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pinsChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pinValidatedForEnrollment(_ message: [String: Any],
	                               resolve: @escaping RCTPromiseResolveBlock,
	                               reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pinValidatedForEnrollment), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pinValidatedForPinChange(_ message: [String: Any],
	                              resolve: @escaping RCTPromiseResolveBlock,
	                              reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pinValidatedForPinChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func pinVerify(_ message: [String: Any],
	               resolve: @escaping RCTPromiseResolveBlock,
	               reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .pinVerify), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func passwordEnroll(_ message: [String: Any],
	                    resolve: @escaping RCTPromiseResolveBlock,
	                    reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .passwordEnroll), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func passwordsChange(_ message: [String: Any],
	                     resolve: @escaping RCTPromiseResolveBlock,
	                     reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .passwordsChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func passwordValidatedForEnrollment(_ message: [String: Any],
	                                    resolve: @escaping RCTPromiseResolveBlock,
	                                    reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .passwordValidatedForEnrollment), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func passwordValidatedForPasswordChange(_ message: [String: Any],
	                                        resolve: @escaping RCTPromiseResolveBlock,
	                                        reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .passwordValidatedForPasswordChange), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func passwordVerify(_ message: [String: Any],
	                    resolve: @escaping RCTPromiseResolveBlock,
	                    reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .passwordVerify), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func accountUsername(_ message: [String: Any],
	                     resolve: @escaping RCTPromiseResolveBlock,
	                     reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .accountUsername), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func cancel(_ message: [String: Any],
	            resolve: @escaping RCTPromiseResolveBlock,
	            reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .cancel), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func isPolicyCompliant(_ message: [String: Any],
	                       resolve: @escaping RCTPromiseResolveBlock,
	                       reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .isPolicyCompliant), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func listenForOsCredentials(_ message: [String: Any],
	                            resolve: @escaping RCTPromiseResolveBlock,
	                            reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .listenForOsCredentials), message: message, resolve: resolve, reject: reject)
	}

	@objc
	func cancelAuthentication(_: [String: Any],
	                          resolve: @escaping RCTPromiseResolveBlock,
	                          reject _: @escaping RCTPromiseRejectBlock)
	{
		// not supported in iOS
		resolve(nil)
	}

	@objc
	func pauseListening(_: [String: Any],
	                    resolve: @escaping RCTPromiseResolveBlock,
	                    reject _: @escaping RCTPromiseRejectBlock)
	{
		// not supported in iOS
		resolve(nil)
	}

	@objc
	func resumeListening(_: [String: Any],
	                     resolve: @escaping RCTPromiseResolveBlock,
	                     reject _: @escaping RCTPromiseRejectBlock)
	{
		// not supported in iOS
		resolve(nil)
	}

	@objc
	func metaData(_ message: [String: Any],
	              resolve: @escaping RCTPromiseResolveBlock,
	              reject: @escaping RCTPromiseRejectBlock)
	{
		handle(ReactMethod(name: .metaData), message: message, resolve: resolve, reject: reject)
	}

	func handle(_ method: ReactMethod, message: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
		guard let messageType = methodMessageMap[method.name] else {
			fatalError("The \(method) method is not implemented!")
		}

		let message = try! JsonMapper.decode(messageType, from: message)
		MethodChannelHandler.shared.add(method: method, resolve: resolve, reject: reject)

		Task {
			switch method.name {
			case .initClient:
				await InitClientMethodHandler().execute(method: method, message: message) { client in
					self.queue.sync {
						self.client = client
					}
				}
			case .metaData:
				await MetaDataMethodHandler().execute(method: method, message: message)
			default:
				guard let client else {
					fatalError("Mobile Authentication Client was not initialized!")
				}

				guard let handler = MethodHandlerFactory.create(by: messageType) else {
					fatalError("Method handler not found for message type \(messageType)!")
				}

				await handler.execute(using: client, method: method, with: message)
			}
		}
	}
}
