//
//  MadStripeSdk.swift
//  MadStripeSdk
//
//  Created by Sergio Herrera on 11/9/21.
//  Copyright © 2021 Facebook. All rights reserved.
//

import StripeTerminal

@objc(MadStripeSdk)
class MadStripeSdk: NSObject, ConnectionTokenProvider, TerminalDelegate, DiscoveryDelegate {
    
    private var pendingConnectionTokenCompletionBlock: ConnectionTokenCompletionBlock?
    private var nativeIsInitialized: Bool = false
    private var pendingDiscoverReaders: Cancelable?
    private var readers: [Reader] = []
    private var currentPaymentIntent: PaymentIntent?
    private var pendingCollectPaymentMethod: Cancelable?
    private var currentUpdate: ReaderSoftwareUpdate?
    
    @objc public static func requiresMainQueueSetup() -> Bool {
       return false
    }
    
    
    ///Initializes the Stripe Terminal SDK.
    @objc func initTerminal() -> Void {
      DispatchQueue.main.async {
         if !self.nativeIsInitialized {
             Terminal.setTokenProvider(self)
             Terminal.shared.delegate = self

             Terminal.setLogListener { logline in
                 self.onLogEntry(logline: logline)
             }
            // To log events from the SDK to the console:
            Terminal.shared.logLevel = .verbose
            self.nativeIsInitialized = true
            
            // When the React module is initialized, abort any pending calls that may not have been
            // cleaned up from a previous initialization (e.g., due to hot reloading).
            self.cancelDiscoverReaders()
         }
      }
    }
    
    ///Checks whether the Stripe Terminal is initialized or not.
    /// - Parameter resolve: when the Terminal is initialized .
    /// - Parameter reject: when the Terminal is not initialized .
    @objc func isInitialized(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) -> Void{
        resolve(nativeIsInitialized)
    }
    
    ///Saves the terminal token
    /// - Parameter token: the token to save.
    /// - Parameter errorMessage: the error message to show.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc(setConnectionToken:withB:withResolver:withRejecter:)
    func setConnectionToken(token: String,errorMessage: String, resolve:RCTPromiseResolveBlock,reject:RCTPromiseRejectBlock) -> Void {
        if let completion = pendingConnectionTokenCompletionBlock {
            if !errorMessage.isEmpty {
                let error = NSError(domain: "com.stripe-terminal.rn",
                                    code: 1,
                                    userInfo: [NSLocalizedDescriptionKey: errorMessage])
                completion(nil, error)
            } else {
                completion(token, nil)
            }

            pendingConnectionTokenCompletionBlock = nil
            resolve(nil)
        }
    }
    
    ///Discovers all the nearby Readers using the given method to do so.
    /// - Parameter method: the way to look for Readers. If null, it's set to Bluetooth scan.
    /// - Parameter simulated:whether we're looking for actual Readers or testing with a simulated one.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc(discoverReaders:withB:withResolver:withRejecter:)
    func discoverReaders(method: Int, simulated: Bool,resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock){
        // Cancel all the previous discovery transactions to avoid corrupt lists.
        cancelDiscoverReaders()
        pendingDiscoverReaders = nil
        let discoveryConfig = DiscoveryConfiguration(discoveryMethod: DiscoveryMethod.init(rawValue: UInt(method)) ?? DiscoveryMethod.bluetoothScan, simulated: simulated)
        
        pendingDiscoverReaders = Terminal.shared.discoverReaders(discoveryConfig, delegate: self, completion: { error in
            self.pendingDiscoverReaders = nil

            if let error = error {
                reject(error.localizedDescription, nil, error)
            } else {
                resolve(nil)
            }
        })
    }
    
    ///Cancels the Reader discovery.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc func cancelDiscoverReaders(_ resolve: RCTPromiseResolveBlock? = nil,rejecter reject: RCTPromiseRejectBlock? = nil){
        if let cancelable = pendingDiscoverReaders {
            cancelable.cancel { error in
                if let error = error {
                    reject?(error.localizedDescription, nil, error)
                } else {
                    resolve?(nil)
                }
            }
            return
        }
        resolve?(nil)
   }
    
    ///Connects to a bluetooth Reader.
    /// - Parameter serialNumber: of the Reader we're attempting to connect.
    /// - Parameter locationId:the id of where the Reader is.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc(connectBluetoothReader:withB:withResolver:withRejecter:)
    func connectBluetoothReader(serialNumber: String,locationId: String, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) {
        //During connection to a simulated Bluetooth reader, we can configure a simulated reader update.
        Terminal.shared.simulatorConfiguration.availableReaderUpdate = .available
        guard let reader = readers.first(where: { $0.serialNumber == serialNumber }) else {
            reject("No reader found",nil,nil)
            return
        }
        
        let connectionConfig = BluetoothConnectionConfiguration(locationId: locationId)

        // this must be run on the main thread
        // https://stackoverflow.com/questions/44767778/main-thread-checker-ui-api-called-on-a-background-thread-uiapplication-appli
        DispatchQueue.main.async {
            Terminal.shared.connectBluetoothReader(reader, delegate: self, connectionConfig: connectionConfig, completion: { reader, error in
                if let reader = reader {
                  resolve([
                        "reader": StripeTerminalUtils.serializeReader(reader: reader),
                    ])
                } else if let error = error {
                    reject(error.localizedDescription, nil, error)
                }
            })
        }
    }
    
    
    ///Connects to an internet Reader.
    /// - Parameter serialNumber: of the Reader we're attempting to connect.
    /// - Parameter failIfInUse:When set to true, the connection will automatically error if the reader is already connected to a device and collecting payment. When set to false, this will allow you to connect to a reader already connected to another device, and will break the existing reader-to-SDK connection on the other device when it attempts to collect payment.
    /// - Default  false
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc(connectInternetReader:withFailIfInUse:withResolver:withRejecter:)
    func connectInternetReader(serialNumber: String,failIfInUse: Bool, resolve: @escaping RCTPromiseResolveBlock,reject: @escaping RCTPromiseRejectBlock) {
        
        guard let reader = readers.first(where: { $0.serialNumber == serialNumber }) else {
            reject("No reader found",nil,nil)
            return
        }

        let connectionConfig = InternetConnectionConfiguration(failIfInUse: failIfInUse)
        
        // this must be run on the main thread
        // https://stackoverflow.com/questions/44767778/main-thread-checker-ui-api-called-on-a-background-thread-uiapplication-appli
        DispatchQueue.main.async {
            Terminal.shared.connectInternetReader(reader, connectionConfig: connectionConfig, completion: { reader, error in
                if let reader = reader {
                    resolve([
                        "reader": StripeTerminalUtils.serializeReader(reader: reader),
                    ])
                } else if let error = error {
                    reject(error.localizedDescription, nil, error)
                }
            })
        }
    }
    
    ///Return the connected Reader.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc func getConnectedReader(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) {
        if let reader = Terminal.shared.connectedReader {
            let reader = StripeTerminalUtils.serializeReader(reader: reader)
            resolve(["reader": reader])
        } else {
            resolve(nil)
        }
    }
    
    ///Disconnect the connected Reader.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed.
    @objc func disconnectReader(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) {
        if Terminal.shared.connectedReader == nil {
            resolve(nil)
            return
        }

        DispatchQueue.main.async {
            Terminal.shared.disconnectReader { error in
                if let error = error {
                    reject(error.localizedDescription, nil, error)
                } else {
                    resolve(nil)
                }
            }
        }
    }
    
    ///Returns the connection status.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed.
    @objc func getConnectionStatus(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) {
        resolve(["status": Terminal.shared.connectionStatus.rawValue])
    }
    
    ///Resets the reader's data.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed.
    @objc func clearCachedCredentials(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) {
       Terminal.shared.clearCachedCredentials()
      resolve(nil)
   }
    
    
    ///Logs the events and notify RN by using the "log" event id
    func onLogEntry(logline : String) {
        EventEmitter.sharedInstance.dispatch(name: "log", body: logline)
    }
    
    
    ///Creates a payment intent.
    /// - Parameter amount: Amount to be debited .
    /// - Parameter currency: Amount's currency with "usd" by defaut.
    /// - Parameter paymentMethodTypes: List of payment method types that this PaymentIntent is allowed to use. The default value is "card_present"
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed.
    @objc(createPaymentIntent:withCurrency:withResolver:withRejecter:)
    func createPaymentIntent(amount: Int, currency: String? = "usd", resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        let paymentMethodTypes = ["card_present"]
        let paymentParams = PaymentIntentParameters(amount: UInt(amount),
                                                    currency: currency!,
                                                    paymentMethodTypes: paymentMethodTypes)
        
        Terminal.shared.createPaymentIntent(paymentParams) { intent, error in
            self.currentPaymentIntent = intent

            if let error = error {
                reject(error.localizedDescription, nil, error)
            } else if let paymentIntent = intent {
                resolve(["intent": StripeTerminalUtils.serializePaymentIntent(intent: paymentIntent)])
                
            }
        }
    }
    
    
    ///Collects the payment method.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed.
   @objc func collectPaymentMethod(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) {
        if let intent = currentPaymentIntent {
            pendingCollectPaymentMethod = Terminal.shared.collectPaymentMethod(intent) { intentWithPaymentMethod, attachError in
                self.pendingCollectPaymentMethod = nil

                if let error = attachError {
                    reject(error.localizedDescription, nil, error)
                } else if let paymentIntent = intentWithPaymentMethod {
                    self.currentPaymentIntent = intentWithPaymentMethod
                    resolve(["intent": StripeTerminalUtils.serializePaymentIntent(intent: paymentIntent)])
                }
            }
        } else {
            reject("There is no active payment intent. Make sure you called createPaymentIntent first",nil,nil)
        }
    }
    
    ///Cancels the collect payment method action.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc func cancelCollectPaymentMethod(_ resolve: RCTPromiseResolveBlock? = nil,rejecter reject: RCTPromiseRejectBlock? = nil){
        if let cancelable = pendingCollectPaymentMethod {
            cancelable.cancel { error in
                if let error = error {
                    reject?(error.localizedDescription, nil, error)
                } else {
                    self.pendingCollectPaymentMethod = nil
                    resolve?(nil)
                }
            }
            return
        }
        resolve?(nil)
   }
    
    
    ///Processes the payment.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc func confirmPaymentIntent(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock) {
        if let intent = currentPaymentIntent {
            Terminal.shared.processPayment(intent) { paymentIntent, error in
                if let error = error {
                    reject(error.localizedDescription, nil, error)
                } else if let paymentIntent = paymentIntent {
                    self.currentPaymentIntent = paymentIntent
                    resolve(["intent": StripeTerminalUtils.serializePaymentIntent(intent: paymentIntent)])
                }
            }
        } else {
            reject("There is no active payment intent. Make sure you called createPaymentIntent first",nil,nil)
        }
    }
    
    
    ///Installls an non mandatory update.
    /// - Parameter resolve: To notify that the process was succeed .
    /// - Parameter reject: To notify that the process failed
    @objc func installAvailableUpdate(_ resolve: @escaping RCTPromiseResolveBlock,rejecter reject: @escaping RCTPromiseRejectBlock){
        if currentUpdate != nil{
            Terminal.shared.installAvailableUpdate()
            resolve(nil)
        }
        
    }
    
    
    
    // MARK: ConnectionTokenProvider
    func fetchConnectionToken(_ completion: @escaping ConnectionTokenCompletionBlock) -> Void {
      pendingConnectionTokenCompletionBlock = completion
      EventEmitter.sharedInstance.dispatch(name: "requestConnectionToken", body: [:])
    }
    
    // MARK: TerminalDelegate
    func terminal(_ terminal: Terminal, didReportUnexpectedReaderDisconnect reader: Reader) {
        EventEmitter.sharedInstance.dispatch(name: "didReportUnexpectedReaderDisconnect", body: ["reader": StripeTerminalUtils.serializeReader(reader: reader)])
    }
    
    public func terminal(_: Terminal, didChangeConnectionStatus status: ConnectionStatus) {
        EventEmitter.sharedInstance.dispatch(name: "didChangeConnectionStatus", body: ["status": status.rawValue])
    }
    
    // MARK: - DiscoveryDelegate
    func terminal(_ terminal: Terminal, didUpdateDiscoveredReaders readers: [Reader]) {
        self.readers = readers
        let readersJSON = readers.map {
            (reader: Reader) -> [String: Any] in
            StripeTerminalUtils.serializeReader(reader: reader)
        }
        EventEmitter.sharedInstance.dispatch(name: "readerDiscoveryCompletion", body: ["readers": readersJSON])
               
    }
    
    
    
}


extension MadStripeSdk: BluetoothReaderDelegate {
    func reader(_ reader: Reader, didReportAvailableUpdate update: ReaderSoftwareUpdate) {
        //Non mandatory updates
        currentUpdate = update
        EventEmitter.sharedInstance.dispatch(name: "didReportAvailableUpdate", body: ["update": StripeTerminalUtils.serializeUpdate(update: update)])
    }

    func reader(_ reader: Reader, didStartInstallingUpdate update: ReaderSoftwareUpdate, cancelable: Cancelable?) {
        // Prevent idle lock while updates are installing
        DispatchQueue.main.async {
            UIApplication.shared.isIdleTimerDisabled = true
            
        }
        currentUpdate = update
        EventEmitter.sharedInstance.dispatch(name: "didStartInstallingUpdate", body: ["update": StripeTerminalUtils.serializeUpdate(update: update)])
    }

    func reader(_ reader: Reader, didFinishInstallingUpdate update: ReaderSoftwareUpdate?, error: Error?) {
        // Prevent idle lock while updates are installing
        UIApplication.shared.isIdleTimerDisabled = false
        
        if let error = error {
            EventEmitter.sharedInstance.dispatch(name: "didFinishInstallingUpdate", body: ["error": error.localizedDescription as Any])
        } else if let update = update {
            EventEmitter.sharedInstance.dispatch(name: "didFinishInstallingUpdate", body: ["update": StripeTerminalUtils.serializeUpdate(update: update)])
            currentUpdate = nil
        }
    }

    func reader(_ reader: Reader, didReportReaderSoftwareUpdateProgress progress: Float) {
        EventEmitter.sharedInstance.dispatch(name: "didReportReaderSoftwareUpdateProgress", body: ["progress": progress])
    }

    func reader(_ reader: Reader, didRequestReaderInput inputOptions: ReaderInputOptions = []) {
        EventEmitter.sharedInstance.dispatch(name: "didRequestReaderInput", body: ["value": inputOptions.rawValue])
    }

    func reader(_ reader: Reader, didRequestReaderDisplayMessage displayMessage: ReaderDisplayMessage) {
        EventEmitter.sharedInstance.dispatch(name: "didRequestReaderDisplayMessage", body: ["value": displayMessage.rawValue])
    }
}
