// 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 Capacitor
import OloPaySDK
import os

/**
 * Please read the Capacitor iOS Plugin Development Guide
 * here: https://capacitorjs.com/docs/plugins/ios
 */
@objc(OloPaySDKPlugin)
public class OloPaySDKPlugin: CAPPlugin, OPApplePayContextDelegate {
    private let _logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "@olo/pay-capacitor")
    
    private var _applePayContext: OPApplePayContext? = nil
    private var _applePayPluginCall: CAPPluginCall? = nil
    private var _applePayPaymentMethod: OloPaySDK.OPPaymentMethodProtocol? = nil
    private var _initializeInternalCalled = false
    
    private let _applePaySemaphore = DispatchSemaphore(value: 1)
    private let _sdkInitializingSemaphore = DispatchSemaphore(value: 1)
    private let _sdkInitializedSemaphore = DispatchSemaphore(value: 1)
    
    // WARNING: NEVER ACCESS/MODIFY THIS VARIABLE DIRECTLY. USE THREAD-SAFE GETTERS AND SETTERS
    private var _sdkInitialized = 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()
        }
    }
    
    @objc func initializeInternal(_ call: CAPPluginCall) {
        if (!_initializeInternalCalled) {
            _initializeInternalCalled = true
            let hybridVersion = call.getString(DataKeys.HybridSdkVersionKey, defaultVersionString)
            let hybridBuildType = call.getString(DataKeys.HybridBuildTypeKey, DataKeys.HybridBuildTypeInternalValue)
            setSdkWrapperInfo(version: hybridVersion, buildType: hybridBuildType)
        }
        
        call.resolve()
    }
    
    @objc func initialize(_ call: CAPPluginCall) {
        let baseError = "Unable to initialize the Olo Pay SDK"
        guard let merchantId = call.getString(DataKeys.ApplePayMerchantIdKey), !merchantId.isEmpty else {
            let message = "\(baseError): Missing parameter \(DataKeys.ApplePayMerchantIdKey)"
            call.reject(message, DataKeys.MissingParameterErrorCode)
            return
        }
        
        guard let companyLabel = call.getString(DataKeys.ApplePayCompanyLabelKey), !companyLabel.isEmpty else {
            let message = "\(baseError): Missing parameter \(DataKeys.ApplePayCompanyLabelKey)"
            call.reject(message, DataKeys.MissingParameterErrorCode)
            return
        }
        
        DispatchQueue.global(qos: .background).async {
            self._sdkInitializingSemaphore.wait()
            
            self.sdkInitialized = false
            let productionEnvironment = call.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()
                call.resolve()
                self.emitDigitalWalletReadyEvent(isReady: OloPayAPI().deviceSupportsApplePay())
            }
        }
    }
    
    @objc func isInitialized(_ call: CAPPluginCall) {
        call.resolve([DataKeys.IsInitializedKey: sdkInitialized])
    }

    @objc func isDigitalWalletInitialized(_ call: CAPPluginCall) {
        call.resolve([DataKeys.IsInitializedKey: sdkInitialized])
    }

    
    @objc func isDigitalWalletReady(_ call: CAPPluginCall) {
        call.resolve([DataKeys.DigitalWalletIsReadyKey: OloPayAPI().deviceSupportsApplePay()])
    }
    
    @objc func getDigitalWalletPaymentMethod(_ call: CAPPluginCall) {
        let baseError = "Unable to create payment method"
        guard sdkInitialized else {
            call.reject("\(baseError): Olo Pay SDK has not been initialized", DataKeys.UninitializedSdkErrorCode)
            return
        }
        
        let oloPayApi = OloPayAPI()
        guard oloPayApi.deviceSupportsApplePay() else {
            call.reject("\(baseError): Apple Pay is not supported on this device", DataKeys.ApplePayUnsupportedErrorCode)
            return
        }
        
        guard let amountDouble = call.getDouble(DataKeys.ApplePayAmountKey) else {
            call.reject("\(baseError): Unable to create payment method. Missing parameter \(DataKeys.ApplePayAmountKey)", DataKeys.MissingParameterErrorCode)
            return
        }
        
        guard amountDouble >= 0 else {
            call.reject("\(baseError): \(DataKeys.ApplePayAmountKey) cannot be negative", DataKeys.InvalidParameter)
            return
        }
        
        DispatchQueue.global(qos: .userInteractive).async {
            self._applePaySemaphore.wait()
            
            let amount = NSDecimalNumber(decimal: Decimal(amountDouble))
            let countryCode = call.getString(DataKeys.ApplePayCountryCodeKey) ?? self.defaultCountryCode
            let currencyCode = call.getString(DataKeys.ApplePayCurrencyCodeKey) ?? 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 {
                    call.reject( "Unexpected Error: Apple Pay Context is nil", DataKeys.GeneralErrorCode)
                            return
                        }
                self._applePayContext = applePayContext
                try self._applePayContext!.presentApplePay()
                self._applePayPluginCall = call
                return
            } catch  {
                errorMessage = error.localizedDescription
            }
            
            let errorData: PluginCallResultData = [
                DataKeys.ErrorMessageKey: errorMessage,
                DataKeys.DigitalWalletTypeKey: DataKeys.DigitalWalletTypeValueKey
            ]
            
            self.applePayCleanup()
            call.resolve(errorData)
        }
    }
    
    public func applePaymentMethodCreated(_ context: OloPaySDK.OPApplePayContextProtocol, didCreatePaymentMethod paymentMethod: OloPaySDK.OPPaymentMethodProtocol) -> NSError? {
        if _applePayPluginCall == nil {
            let message = "Unexpected error: Saved plugin call 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 applePayPluginCall = _applePayPluginCall else {
            // If there is no saved plugin call then there is nothing to be done except log the problem and return
            _logger.error("Unexpected error: Saved plugin call is nil")
            applePayCleanup()
            return
        }
        
        var applePayData: PluginCallResultData? = 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 {
            let paymentMethodData: [String : Any] = [
                DataKeys.IDKey: _applePayPaymentMethod!.id,
                DataKeys.Last4Key: _applePayPaymentMethod!.last4 ?? "",
                DataKeys.CardTypeKey: _applePayPaymentMethod!.cardType.description,
                DataKeys.ExpirationMonthKey: _applePayPaymentMethod!.expirationMonth ?? "",
                DataKeys.ExpirationYearKey: _applePayPaymentMethod!.expirationYear ?? "",
                DataKeys.PostalCodeKey: _applePayPaymentMethod!.postalCode ?? "",
                DataKeys.CountryCodeKey: _applePayPaymentMethod!.country ?? "",
                DataKeys.IsDigitalWalletKey: _applePayPaymentMethod!.isApplePay,
                DataKeys.ProductionEnvironmentKey: _applePayPaymentMethod!.environment == .production
            ]
            
            applePayData = [DataKeys.PaymentMethodKey: paymentMethodData]
        }
        
        applePayCleanup()
        
        if let applePayDataUnwrapped = applePayData {
            applePayPluginCall.resolve(applePayDataUnwrapped)
        } else {
            applePayPluginCall.resolve() //Indicates user cancellation
        }
    }
    
    private func applePayCleanup() {
        _applePayContext = nil
        _applePayPaymentMethod = nil
        _applePayPluginCall = 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: .capacitor
        )
        
        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"
}
