// Copyright © 2022 Olo Inc. All rights reserved.
// This software is made available under the Olo Pay SDK License (See LICENSE.md file)
import Foundation
import OloPaySDK
import os

@objc(OlopaysdkReactNative)
class OlopaysdkReactNative: NSObject, OPApplePayContextDelegate {
    private let _logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "@olo/pay-react-native")

    private var _applePayContext: OPApplePayContext? = nil
    private var _applePayResolver: RCTPromiseResolveBlock? = nil
    private var _applePayPaymentMethod: OloPaySDK.OPPaymentMethodProtocol? = nil
    private var _initializeMetadataCalled = false
    
    private let _applePaySemaphore = DispatchSemaphore(value: 1)
    private let _sdkInitializingSemaphore = DispatchSemaphore(value: 1)
    private let _sdkInitializedSemaphore = DispatchSemaphore(value: 1)

    // WARNING: NEVER ACCESS/MODIFY THESE VARIABLES DIRECTLY. USE THREAD-SAFE GETTERS AND SETTERS
    private var _sdkInitialized = false
    private var _applePayInitialized = false

    private var sdkInitialized: Bool {
        get {
            self._sdkInitializedSemaphore.wait()
            let value = _sdkInitialized
            self._sdkInitializedSemaphore.signal()
            return value
        }
        set(newValue) {
            self._sdkInitializedSemaphore.wait()
            _sdkInitialized = newValue
            self._sdkInitializedSemaphore.signal()
        }
    }
    
    private var applePayInitialized: Bool {
        get {
            self._applePaySemaphore.wait()
            let value = _applePayInitialized
            self._applePaySemaphore.signal()
            return value
        }
        set(newValue) {
            self._applePaySemaphore.wait()
            _applePayInitialized = newValue
            self._applePaySemaphore.signal()
        }
    }

    @objc(initializeOloPay:withResolver:withRejecter:)
    func initializeOloPay(args: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        let merchantId = args.getString(DataKeys.ApplePayMerchantIdKey)
        let companyLabel = args.getString(DataKeys.ApplePayCompanyLabelKey)
        
        DispatchQueue.global(qos: .background).async {
            self._sdkInitializingSemaphore.wait()

            self.sdkInitialized = false
            self.applePayInitialized = false
            let productionEnvironment = args.getBool(DataKeys.ProductionEnvironmentKey) ?? self.defaultProductionEnvironment
            
            let setupParameters = OPSetupParameters(
                withEnvironment: productionEnvironment ? .production : .test,
                withApplePayMerchantId: merchantId,
                withApplePayCompanyLabel: companyLabel)

            OloPayApiInitializer().setup(with: setupParameters) {
                self.sdkInitialized = true
                self._sdkInitializingSemaphore.signal()
                
                let hasMerchantId = !merchantId.isEmptyOrNil
                let hasCompanyLabel = !companyLabel.isEmptyOrNil
                let initializeApplePay = hasMerchantId && hasCompanyLabel
                let applePayError = (hasMerchantId && !hasCompanyLabel) || (!hasMerchantId && hasCompanyLabel)
                
                if applePayError {
                    let missingParam = hasMerchantId ? DataKeys.ApplePayCompanyLabelKey : DataKeys.ApplePayMerchantIdKey
                    let message = "Unable to initialize Apple Pay: Missing parameter: \(missingParam)"
                    reject(ErrorCodes.MissingParameter, message, nil)
                    return
                }
                
                resolve(nil)
                
                if initializeApplePay {
                    self.applePayInitialized = true
                    OloPayEventEmitter.emitDigitalWalletReadyEvent(isReady: OloPayAPI().deviceSupportsApplePay())
                }
            }
        }
    }
    
    @objc(initializeMetadata:withResolver:withRejecter:)
    func initializeMetadata(_ args: NSDictionary,resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) {
            if (!_initializeMetadataCalled) {
                _initializeMetadataCalled = true
                let hybridVersion = args.getString(DataKeys.HybridSdkVersionKey, defaultValue: defaultVersionString)
                let hybridBuildType = args.getString(DataKeys.HybridBuildTypeKey, defaultValue: DataKeys.HybridBuildTypeInternalValue)
                setSdkWrapperInfo(version: hybridVersion, buildType: hybridBuildType)
            }

            resolve(nil)
        }

    @objc(isOloPayInitialized:withRejecter:)
    func isOloPayInitialized(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        resolve([DataKeys.IsInitializedKey: sdkInitialized])
    }

    @objc(isDigitalWalletInitialized:withRejecter:)
    func isDigitalWalletInitialized(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        resolve([DataKeys.IsInitializedKey: applePayInitialized])
    }

    @objc(isDigitalWalletReady:withRejecter:)
    func isDigitalWalletReady(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        resolve([DataKeys.DigitalWalletIsReadyKey: applePayInitialized && OloPayAPI().deviceSupportsApplePay()])
    }

    @objc(initializeGooglePay:withResolver:withRejecter:)
    func initializeGooglePay(args: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        reject(ErrorCodes.Unimplemented, "initializeGooglePay() is not implemented on iOS", nil)
    }

    @objc(changeGooglePayVendor:withResolver:withRejecter:)
    func changeGooglePayVendor(args: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
        reject(ErrorCodes.Unimplemented, "changeGooglePayVendor() is not implemented on iOS", nil)
    }

    @objc(getDigitalWalletPaymentMethod:withResolver:withRejecter:)
    func getDigitalWalletPaymentMethod(args: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
        let baseError = "Unable to create payment method"
        guard sdkInitialized else {
            reject(ErrorCodes.UninitializedSdk, "\(baseError): Olo Pay SDK has not been initialized", nil)
            return
        }

        let oloPayApi = OloPayAPI()
        guard oloPayApi.deviceSupportsApplePay() else {
            reject(ErrorCodes.ApplePayUnsupported, "\(baseError): Apple Pay is not supported on this device", nil)
            return
        }

        guard let amountDouble = args.getDouble(DataKeys.ApplePayAmountKey) else {
            reject(ErrorCodes.MissingParameter, "\(baseError): Unable to create payment method. Missing parameter \(DataKeys.ApplePayAmountKey)", nil)
            return
        }
        
        guard amountDouble >= 0 else {
            reject(ErrorCodes.InvalidParameter, "\(baseError): \(DataKeys.ApplePayAmountKey) cannot be negative", nil)
            return
        }

        DispatchQueue.global(qos: .userInteractive).async {
            self._applePaySemaphore.wait()

            let amount = NSDecimalNumber(decimal: Decimal(amountDouble))
            let countryCode = args.getString(DataKeys.ApplePayCountryCodeKey, defaultValue: self.defaultCountryCode)
            let currencyCode = args.getString(DataKeys.ApplePayCurrencyCodeKey, defaultValue: self.defaultCurrencyCode)
            var errorMessage: String

            do {
                let paymentRequest = try oloPayApi.createPaymentRequest(forAmount: amount, inCountry: countryCode, withCurrency: currencyCode)
                guard let applePayContext = OPApplePayContext(paymentRequest: paymentRequest, delegate: self) else {
                    reject(ErrorCodes.GeneralError, "Unexpected Error: Apple Pay Context is nil", nil)
                    return
                }
                self._applePayContext = applePayContext
                try self._applePayContext!.presentApplePay()
                self._applePayResolver = resolve
                return
            } catch  {
                errorMessage = error.localizedDescription
            }

            let errorData = [
                DataKeys.ErrorMessageKey: errorMessage,
                DataKeys.DigitalWalletTypeKey: DataKeys.DigitalWalletTypeValueKey
            ]

            self.applePayCleanup()
            resolve(errorData)
        }
    }

    public func applePaymentMethodCreated(_ context: OloPaySDK.OPApplePayContextProtocol, didCreatePaymentMethod paymentMethod: OloPaySDK.OPPaymentMethodProtocol) -> NSError? {
        guard _applePayResolver != nil else {
            let message = "Unexpected error: Saved method call resolver is nil"
            _logger.error("\(message)")
            return OPError(errorType: .generalError, description: message)
        }

        _applePayPaymentMethod = paymentMethod

        // NOTE: This will trigger a success flow in the Apple Pay Sheet... If the actual order placement fails with the Olo Odering API it
        // will be up to the app developer to display a different error in their app to let the end user know that the Apple Pay flow did not
        // succeed. The only way around this limitation would be to make the call to the basket submit endpoint to the olo ordering api for
        // them. We would have to take as method parameters everything needed to make the api call on their behalf, and return the result of
        // the API call to them. This would be a rather large undertaking to get it right
        return nil
    }
    
    public func applePaymentCompleted(_ context: OPApplePayContextProtocol, didCompleteWith status: OPPaymentStatus, error: Error?) {
        guard let applePayResolver = _applePayResolver else {
            // If there is no saved resolver there is nothing to be done except log the problem and return
            _logger.error("Unexpected error: Saved method call resolver is nil")
            applePayCleanup()
            return
        }

        var applePayData: NSDictionary? = nil

        if status == .error {
            applePayData = [
                DataKeys.ErrorMessageKey: error!.localizedDescription,
                DataKeys.DigitalWalletTypeKey: DataKeys.DigitalWalletTypeValueKey
            ]
        } else if status == .success && _applePayPaymentMethod == nil {
            applePayData = [
                DataKeys.ErrorMessageKey: "Unexpected error: Payment method is nil",
                DataKeys.DigitalWalletTypeKey: DataKeys.DigitalWalletTypeValueKey
            ]
        } else if status == .success && _applePayPaymentMethod != nil {
            applePayData = [DataKeys.PaymentMethodKey: _applePayPaymentMethod!.toDictionary()]
        }

        applePayCleanup()
        applePayResolver(applePayData)
    }

    private func applePayCleanup() {
        _applePayContext = nil
        _applePayPaymentMethod = nil
        _applePayResolver = nil
        _applePaySemaphore.signal()
    }
    
    private func setSdkWrapperInfo(version: String, buildType: String) {
        let versionStrings = version.components(separatedBy: ".")
        let majorVersionString = versionStrings.count > MajorVersionIndex ? versionStrings[MajorVersionIndex] : "0"
        let minorVersionString = versionStrings.count > MinorVersionIndex ? versionStrings[MinorVersionIndex] : "0"
        let buildVersionString = versionStrings.count > BuildVersionIndex ? versionStrings[BuildVersionIndex] : "0"

        let sdkWrapperInfo = OPSdkWrapperInfo(
            withMajorVersion: Int(majorVersionString) ?? 0,
            withMinorVersion: Int(minorVersionString) ?? 0,
            withBuildVersion: Int(buildVersionString) ?? 0,
            withSdkBuildType: buildType == DataKeys.HybridBuildTypePublicValue ? .publicBuild : .internalBuild,
            withSdkPlatform: .reactNative
        )

        OloPayAPI.sdkWrapperInfo = sdkWrapperInfo
    }

    // Default values
    private let defaultProductionEnvironment = true
    private let defaultCountryCode = "US"
    private let defaultCurrencyCode = "USD"
    private let MajorVersionIndex = 0
    private let MinorVersionIndex = 1
    private let BuildVersionIndex = 2
    private let defaultVersionString = "0.0.0"
}
