//
//  ApplePayPlugin.swift
//  Astro
//
//  Created by Karl Schmidt on 2015-11-16.
//  Copyright © 2015 Mobify Research & Development Inc. All rights reserved.
//

import PassKit

class CompletionHandlerMap<T> {
    private var handlerMap = [Int: T]()
    private var nextHandlerId = 0

    @discardableResult
    func addCompletionHandler(_ handler: T) -> Int {
        let handlerId = nextHandlerId
        handlerMap[handlerId] = handler
        nextHandlerId += 1
        return handlerId
    }

    func getCompletionHandler(_ handlerId: Int) -> T? {
        return handlerMap[handlerId]
    }

    func removeCompletionHandler(_ handlerId: Int) {
        handlerMap.removeValue(forKey: handlerId)
    }
}

public class ApplePayPlugin: Plugin, PKPaymentAuthorizationViewControllerDelegate, UIAlertViewDelegate {
    typealias DidAuthorizePaymentCompletionType = (PKPaymentAuthorizationStatus) -> Void
    typealias DidSelectShippingAddressCompletionType = (PKPaymentAuthorizationStatus, [PKShippingMethod], [PKPaymentSummaryItem]) -> Void
    typealias DidSelectShippingMethodCompletionType = (PKPaymentAuthorizationStatus, [PKPaymentSummaryItem]) -> Void

    private var didAuthorizePaymentCompletionHandlers = CompletionHandlerMap<DidAuthorizePaymentCompletionType>()
    private var didSelectShippingAddressCompletionHandlers = CompletionHandlerMap<DidSelectShippingAddressCompletionType>()
    private var didSelectShippingMethodCompletionHandlers = CompletionHandlerMap<DidSelectShippingMethodCompletionType>()

    private static let completionKey = "completionId"
    private static let authorizationStatusKey = "authorizationStatus"
    private static let shippingMethodsKey = "shippingMethods"
    private static let paymentSummaryItemsKey = "paymentSummaryItems"
    private static let merchantCapabilitiesKey = "merchantCapabilities"
    private static let merchantIdentifierKey = "merchantIdentifier"
    private static let countryCodeKey = "countryCode"
    private static let currencyCodeKey = "currencyCode"
    private static let supportedNetworksKey = "supportedNetworks"
    private static let requiredBillingAddressFieldsKey = "requiredBillingAddressFields"
    private static let requiredShippingAddressFieldsKey = "requiredShippingAddressFields"

    // The currency amounts coming from Javascript will be in cents, so use the follow to convert appropriately
    private static let currencyConversion = NSDecimalNumber(value: 0.01)

    public required init(address: MessageAddress, messageBus: MessageBus, pluginResolver: PluginResolver, options: JSONObject?) {
        super.init(address: address, messageBus: messageBus, pluginResolver: pluginResolver, options: options)

        self.addRpcMethodShim("getAvailability") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let networks: [String] = MethodShimUtils.getArg(params, key: "networks", respond: respond) {
                self.getAvailability(networks, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("setUpApplePay") { _, respond in
            ////////// This will be autogenerated at some point //////////
            self.setUpApplePay(respond)
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("startPayment") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let paymentRequestData: JSONObject = MethodShimUtils.getArg(params, key: "paymentRequest", respond: respond) {
                self.startPayment(paymentRequestData, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("didAuthorizePaymentCompletion") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let completion: NSNumber = MethodShimUtils.getArg(params, key: ApplePayPlugin.completionKey, respond: respond),
               let rawAuthorizationStatus: String = MethodShimUtils.getArg(params, key: ApplePayPlugin.authorizationStatusKey, respond: respond) {
                   self.didAuthorizePaymentCompletion(completion, rawAuthorizationStatus: rawAuthorizationStatus, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("didSelectShippingAddressCompletion") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let completion: NSNumber = MethodShimUtils.getArg(params, key: ApplePayPlugin.completionKey, respond: respond),
               let rawAuthorizationStatus: String = MethodShimUtils.getArg(params, key: ApplePayPlugin.authorizationStatusKey, respond: respond),
               let rawShippingMethods: [NSDictionary] = MethodShimUtils.getArg(params, key: ApplePayPlugin.shippingMethodsKey, respond: respond),
               let rawPaymentSummaryItems: [NSDictionary] = MethodShimUtils.getArg(params, key: ApplePayPlugin.paymentSummaryItemsKey, respond: respond) {
                   self.didSelectShippingAddressCompletion(completion, rawAuthorizationStatus: rawAuthorizationStatus, rawShippingMethods: rawShippingMethods, rawPaymentSummaryItems: rawPaymentSummaryItems, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }

        self.addRpcMethodShim("didSelectShippingMethodCompletion") { params, respond in
            ////////// This will be autogenerated at some point //////////
            if let completion: NSNumber = MethodShimUtils.getArg(params, key: ApplePayPlugin.completionKey, respond: respond),
               let rawAuthorizationStatus: String = MethodShimUtils.getArg(params, key: ApplePayPlugin.authorizationStatusKey, respond: respond),
               let rawPaymentSummaryItems: [NSDictionary] = MethodShimUtils.getArg(params, key: ApplePayPlugin.paymentSummaryItemsKey, respond: respond) {
                   self.didSelectShippingMethodCompletion(completion, rawAuthorizationStatus: rawAuthorizationStatus, rawPaymentSummaryItems: rawPaymentSummaryItems, respond: respond)
            }
            /////////////////////////////////////////////////////////////
        }
    }

    // @RpcMethod
    func startPayment(_ paymentRequestData: JSONObject, respond: RPCMethodCallback) {
        let paymentRequest = PKPaymentRequest()

        guard let merchantIdentifier = paymentRequestData[ApplePayPlugin.merchantIdentifierKey] as? String else {
            respond(.error("Missing \(ApplePayPlugin.merchantIdentifierKey)"))
            return
        }
        guard let rawPaymentSummaryItems = paymentRequestData[ApplePayPlugin.paymentSummaryItemsKey] as? [NSDictionary] else {
            respond(.error("Missing \(ApplePayPlugin.paymentSummaryItemsKey)"))
            return
        }
        guard let paymentSummaryItems = ApplePayPlugin.parsePaymentSummaryItems(rawPaymentSummaryItems, respond: respond) else {
            return
        }
        guard let countryCode = paymentRequestData[ApplePayPlugin.countryCodeKey] as? String else {
            respond(.error("Missing \(ApplePayPlugin.countryCodeKey)"))
            return
        }
        guard let currencyCode = paymentRequestData[ApplePayPlugin.currencyCodeKey] as? String else {
            respond(.error("Missing \(ApplePayPlugin.currencyCodeKey)"))
            return
        }
        guard let supportedNetworks = paymentRequestData[ApplePayPlugin.supportedNetworksKey] as? [String] else {
            respond(.error("Missing \(ApplePayPlugin.supportedNetworksKey)"))
            return
        }
        guard let rawMerchantCapabilities = paymentRequestData[ApplePayPlugin.merchantCapabilitiesKey] as? [String] else {
            respond(.error("Missing \(ApplePayPlugin.merchantCapabilitiesKey)"))
            return
        }
        guard let merchantCapabilities = ApplePayPlugin.parseMerchantCapabilities(rawMerchantCapabilities, respond: respond) else {
            return
        }

        if let rawRequiredBillingAddressFields = paymentRequestData[ApplePayPlugin.requiredBillingAddressFieldsKey] as? [String] {
            guard let requiredBillingAddressFields = ApplePayPlugin.parseRequiredAddressFields(rawRequiredBillingAddressFields, respond: respond) else {
                return
            }
            paymentRequest.requiredBillingAddressFields = requiredBillingAddressFields
        }
        if let rawRequiredShippingAddressFields = paymentRequestData[ApplePayPlugin.requiredShippingAddressFieldsKey] as? [String] {
            guard let requiredShippingAddressFields = ApplePayPlugin.parseRequiredAddressFields(rawRequiredShippingAddressFields, respond: respond) else {
                return
            }
            paymentRequest.requiredShippingAddressFields = requiredShippingAddressFields
        }
        if let rawShippingMethods = paymentRequestData[ApplePayPlugin.shippingMethodsKey] as? [NSDictionary] {
            guard let shippingMethods = ApplePayPlugin.parseShippingMethods(rawShippingMethods, respond: respond) else {
                return
            }
            paymentRequest.shippingMethods = shippingMethods
        }

        paymentRequest.merchantIdentifier = merchantIdentifier
        paymentRequest.paymentSummaryItems = paymentSummaryItems
        paymentRequest.countryCode = countryCode
        paymentRequest.currencyCode = currencyCode
        let networks = supportedNetworks.map {
            return PKPaymentNetwork($0)
        }
        paymentRequest.supportedNetworks = networks
        paymentRequest.merchantCapabilities = merchantCapabilities

        // Apple Pay doesn't seem to be very Swifty...the constructor can fail if the data is invalid,
        // and will return a nil, unwrapped object. So we force it to be optional, then unwrap it afterwards
        let paymentViewController: PKPaymentAuthorizationViewController? = PKPaymentAuthorizationViewController(paymentRequest: paymentRequest)

        guard let unwrappedPaymentViewController = paymentViewController else {
            respond(.error("Unable to create payment request"))
            return
        }

        unwrappedPaymentViewController.delegate = self

        // Don't guard and bail here, since we could be running headless (tests)
        if let currentViewController = UIApplication.shared.currentViewController {
            // JS COORDINATION NECESSARY FOR CORRECT BEHAVIOUR
            // It is possible to hang a modal view plugin if it's show method is called
            // while another instance is dismissing.
            // currentViewController will point to the dismissing view controller and then
            // the call to currentViewController.presentViewController below will hang
            // because the instance that is trying to present it is in the middle of being
            // dismissed.

            currentViewController.present(unwrappedPaymentViewController, animated: true, completion: nil)
        }
    }

    @objc static func canBeSetUp() -> Bool {
        return PKPaymentAuthorizationViewController.canMakePayments()
    }

    // @RpcMethod
    func getAvailability(_ networks: [String], respond: RPCMethodCallback) {
        let paymentNetworks = networks.map {
            return PKPaymentNetwork($0)
        }
        if PKPaymentAuthorizationViewController.canMakePayments(usingNetworks:paymentNetworks) {
            respond(.result("ready"))
        } else if ApplePayPlugin.canBeSetUp() {
            respond(.result("can_set_up"))
        } else {
            respond(.result("unavailable"))
        }
    }

    // @RpcMethod
    func setUpApplePay(_ respond: RPCMethodCallback) {
        if #available(iOS 8.3, *) {
            PKPassLibrary().openPaymentSetup()
        } else {
            respond(.error("Apple Pay is not supported on this device"))
        }
    }

    public func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment, completion: @escaping (PKPaymentAuthorizationStatus) -> Void) {
        let params: JSONObject = [
            "payment": payment.dictionaryValue
        ]
        triggerEventWithCompletion(completion, params: params, completionHandlers: &didAuthorizePaymentCompletionHandlers, eventName: "didAuthorizePayment")
    }

    // @RpcMethod
    func didAuthorizePaymentCompletion(_ completionId: NSNumber, rawAuthorizationStatus: String, respond: RPCMethodCallback ) {
        let authorizationStatus = ApplePayPlugin.parsePKPaymentAuthorizationStatus(rawAuthorizationStatus, respond: respond)
        didAuthorizePaymentCompletion(completionId, authorizationStatus: authorizationStatus, respond: respond)
    }

    private func didAuthorizePaymentCompletion(_ completionId: NSNumber, authorizationStatus: PKPaymentAuthorizationStatus, respond: RPCMethodCallback) {
        if let completion = didAuthorizePaymentCompletionHandlers.getCompletionHandler(completionId.intValue) {
            didAuthorizePaymentCompletionHandlers.removeCompletionHandler(completionId.intValue)
            completion(authorizationStatus)
        } else {
            respond(.error("Unable to call authorize payment completion handler"))
        }
    }

    public func paymentAuthorizationViewControllerDidFinish(_ controller: PKPaymentAuthorizationViewController) {
        controller.dismiss(animated: true, completion: nil)
        trigger("paymentAuthorizationViewControllerDidFinish")
    }

    public func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didSelectShippingContact contact: PKContact, completion: @escaping (PKPaymentAuthorizationStatus, [PKShippingMethod], [PKPaymentSummaryItem]) -> Void) {
        let params = [
            "address": contact.dictionaryValue
        ]

        triggerEventWithCompletion(completion, params: params, completionHandlers: &didSelectShippingAddressCompletionHandlers, eventName: "didSelectShippingAddress")
    }

    // @RpcMethod
    func didSelectShippingAddressCompletion(_ completionId: NSNumber, rawAuthorizationStatus: String, rawShippingMethods: [NSDictionary], rawPaymentSummaryItems: [NSDictionary], respond: RPCMethodCallback) {
        let authorizationStatus = ApplePayPlugin.parsePKPaymentAuthorizationStatus(rawAuthorizationStatus, respond: respond)
        if let shippingMethods = ApplePayPlugin.parseShippingMethods(rawShippingMethods, respond: respond),
           let paymentSummaryItems = ApplePayPlugin.parsePaymentSummaryItems(rawPaymentSummaryItems, respond: respond) {
            didSelectShippingAddressCompletion(completionId, authorizationStatus: authorizationStatus, shippingMethods: shippingMethods, paymentSummaryItems: paymentSummaryItems, respond: respond)
        }
    }

    private func didSelectShippingAddressCompletion(_ completionId: NSNumber, authorizationStatus: PKPaymentAuthorizationStatus, shippingMethods: [PKShippingMethod], paymentSummaryItems: [PKPaymentSummaryItem], respond: RPCMethodCallback) {
        if let completion = didSelectShippingAddressCompletionHandlers.getCompletionHandler(completionId.intValue) {
            didSelectShippingAddressCompletionHandlers.removeCompletionHandler(completionId.intValue)
            completion(authorizationStatus, shippingMethods, paymentSummaryItems)
        } else {
            respond(.error("Unable to call shipping address select completion handler"))
        }
    }

    public func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didSelect shippingMethod: PKShippingMethod, completion: @escaping (PKPaymentAuthorizationStatus, [PKPaymentSummaryItem]) -> Void) {
        let params: JSONObject = [
            "shippingMethod": shippingMethod.dictionaryValue
        ]

        triggerEventWithCompletion(completion, params: params, completionHandlers: &didSelectShippingMethodCompletionHandlers, eventName: "didSelectShippingMethod")
    }

    // @RpcMethod
    func didSelectShippingMethodCompletion(_ completionId: NSNumber, rawAuthorizationStatus: String, rawPaymentSummaryItems: [NSDictionary], respond: RPCMethodCallback) {
        let authorizationStatus = ApplePayPlugin.parsePKPaymentAuthorizationStatus(rawAuthorizationStatus, respond: respond)
        if let paymentSummaryItems = ApplePayPlugin.parsePaymentSummaryItems(rawPaymentSummaryItems, respond: respond) {
            didSelectShippingMethodCompletion(completionId, authorizationStatus: authorizationStatus, paymentSummaryItems: paymentSummaryItems, respond: respond)
        }
    }

    private func didSelectShippingMethodCompletion(_ completionId: NSNumber, authorizationStatus: PKPaymentAuthorizationStatus, paymentSummaryItems: [PKPaymentSummaryItem], respond: RPCMethodCallback) {
        if let completion = didSelectShippingMethodCompletionHandlers.getCompletionHandler(completionId.intValue) {
            didSelectShippingMethodCompletionHandlers.removeCompletionHandler(completionId.intValue)
            completion(authorizationStatus, paymentSummaryItems)
        } else {
            respond(.error("Unable to call shipping method select completion handler"))
        }
    }

    private func triggerEventWithCompletion<T>(_ completion: T, params: JSONObject, completionHandlers: inout CompletionHandlerMap<T>, eventName: String) -> Void {
        var triggerParams = params

        let completionAddress = completionHandlers.addCompletionHandler(completion)
        triggerParams[ApplePayPlugin.completionKey] = NSNumber(value: completionAddress)

        trigger(eventName, params: triggerParams)
    }

    private static func parseShippingMethods(_ rawShippingMethods: [NSDictionary], respond: RPCMethodCallback) -> [PKShippingMethod]? {
        var shippingMethods = [PKShippingMethod]()
        for rawShippingMethod in rawShippingMethods {
            if let label = rawShippingMethod["label"] as? String,
               let amount = rawShippingMethod["amount"] as? NSNumber,
               let identifier = rawShippingMethod["identifier"] as? String {
                let shippingMethod = PKShippingMethod(label: label,
                                                      amount: NSDecimalNumber(decimal: amount.decimalValue).multiplying(by: currencyConversion))
                shippingMethod.identifier = identifier
                if let detail = rawShippingMethod["detail"] as? String {
                    shippingMethod.detail = detail
                }
                shippingMethods.append(shippingMethod)
            } else {
                respond(.error("Unable to parse \(shippingMethodsKey)"))
                return nil
            }
        }

        return shippingMethods
    }

    private static func parsePaymentSummaryItems(_ rawSummaryItems: [NSDictionary], respond: RPCMethodCallback) -> [PKPaymentSummaryItem]? {
        var paymentSummaryItems = [PKPaymentSummaryItem]()
        for rawSummaryItem in rawSummaryItems {
            if let label = rawSummaryItem["label"] as? String,
                let amount = rawSummaryItem["amount"] as? NSNumber {
                    paymentSummaryItems.append(PKPaymentSummaryItem(label: label,
                                                                    amount: NSDecimalNumber(decimal: amount.decimalValue).multiplying(by: currencyConversion)))
            } else {
                respond(.error("Unable to parse \(paymentSummaryItemsKey)"))
                return nil
            }
        }

        return paymentSummaryItems
    }

    private static func parsePKPaymentAuthorizationStatus(_ authStatus: String, respond: RPCMethodCallback) -> PKPaymentAuthorizationStatus {
        switch authStatus {
        case "Success":
            return PKPaymentAuthorizationStatus.success
        case "Failure":
            return PKPaymentAuthorizationStatus.failure
        case "InvalidBillingPostalAddress":
            return PKPaymentAuthorizationStatus.invalidBillingPostalAddress
        case "InvalidShippingPostalAddress":
            return PKPaymentAuthorizationStatus.invalidShippingPostalAddress
        case "InvalidShippingContact":
            return PKPaymentAuthorizationStatus.invalidShippingContact
        default:
            respond(.error("Unable to parse \(authorizationStatusKey): \(authStatus)"))
            return PKPaymentAuthorizationStatus.failure
        }
    }

    private static func parseMerchantCapabilities(_ rawMerchantCapabilities: [String], respond: RPCMethodCallback) -> PKMerchantCapability? {
        var merchantCapabilities = PKMerchantCapability()
        for merchantCapability in rawMerchantCapabilities {
            switch merchantCapability {
            case "3DS":
                merchantCapabilities.formUnion(.capability3DS)
                break
            case "EMV":
                merchantCapabilities.formUnion(.capabilityEMV)
            default:
                respond(.error("Unknown vendor capability: \(merchantCapability)"))
                return nil
            }
        }
        return merchantCapabilities
    }

    private static func parseRequiredAddressFields(_ rawRequiredAddressFields: [String], respond: RPCMethodCallback) -> PKAddressField? {
        var requiredAddressFields = PKAddressField()
        for requiredAddressField in rawRequiredAddressFields {
            switch requiredAddressField {
            case "None":
                requiredAddressFields.formUnion(PKAddressField())
                break
            case "PostalAddress":
                requiredAddressFields.formUnion(.postalAddress)
            case "Phone":
                requiredAddressFields.formUnion(.phone)
            case "Email":
                requiredAddressFields.formUnion(.email)
            default:
                respond(.error("Unknown required address field: \(requiredAddressField)"))
                return nil
            }
        }
        return requiredAddressFields
    }
}

private extension PKContact {
    var dictionaryValue: [String: String?] {
        let dict = [
            "firstName": self.name?.givenName,
            "lastName": self.name?.familyName,
            "phone": self.phoneNumber?.stringValue,
            "address1": self.postalAddress?.street,
            "address2": "",
            "countryCode": self.postalAddress?.isoCountryCode.uppercased(),
            "country": self.postalAddress?.country,
            "postal": self.postalAddress?.postalCode,
            "state": self.postalAddress?.state,
            "city": self.postalAddress?.city
        ]
        return dict
    }
}

private extension PKShippingMethod {
    var dictionaryValue: [String: AnyHashable] {
        let identifier = self.identifier ?? ""
        let detail = self.detail ?? ""
        return ["label": self.label,
                "amount": self.amount,
                "identifier": identifier,
                "detail": detail]
    }
}

private extension PKPayment {
    var dictionaryValue: [String: Any] {
        var paymentDictionary = [String: Any]()

        if let shippingContact = self.shippingContact {
            paymentDictionary["shippingAddress"] = shippingContact.dictionaryValue
        }

        if let billingContact = self.billingContact {
            paymentDictionary["billingAddress"] = billingContact.dictionaryValue
        }

        if let shippingMethod = self.shippingMethod {
            paymentDictionary["shippingMethod"] = shippingMethod.dictionaryValue
        }

        let paymentToken = [
            "paymentInstrumentName": self.token.paymentMethod.displayName,
            "paymentNetwork": self.token.paymentMethod.network?.rawValue,
            "transactionIdentifier": self.token.transactionIdentifier,
            "paymentData": self.token.paymentData.base64EncodedString(options: NSData.Base64EncodingOptions())
        ]

        paymentDictionary["token"] = paymentToken

        return paymentDictionary
    }
}
