//
//  STPSource.swift
//  Stripe
//
//  Created by Ben Guo on 1/23/17.
//  Copyright © 2017 Stripe, Inc. All rights reserved.
//

import Foundation
import UIKit

/// Representation of a customer's payment instrument created with the Stripe API. - seealso: https://stripe.com/docs/api#sources
public class STPSource: NSObject, STPAPIResponseDecodable, STPSourceProtocol, STPPaymentOption {

    /// You cannot directly instantiate an `STPSource`. You should only use one that
    /// has been returned from an `STPAPIClient` callback.
    override required init() {
        super.init()
    }

    /// The amount associated with the source.
    @objc public private(set) var amount: NSNumber?
    /// The client secret of the source. Used for client-side fetching of a source
    /// using a publishable key.
    @objc public private(set) var clientSecret: String?
    /// When the source was created.
    @objc public private(set) var created: Date?
    /// The currency associated with the source.
    @objc public private(set) var currency: String?
    /// The authentication flow of the source.
    @objc public private(set) var flow: STPSourceFlow = .none
    /// Whether or not this source was created in livemode.
    @objc public private(set) var livemode = false
    /// Information about the owner of the payment instrument.
    @objc public private(set) var owner: STPSourceOwner?
    /// Information related to the receiver flow. Present if the source's flow
    /// is receiver.
    @objc public private(set) var receiver: STPSourceReceiver?
    /// Information related to the redirect flow. Present if the source's flow
    /// is redirect.
    @objc public private(set) var redirect: STPSourceRedirect?
    /// The status of the source.
    @objc public private(set) var status: STPSourceStatus = .unknown
    /// The type of the source.
    @objc public private(set) var type: STPSourceType = .unknown
    /// Whether this source should be reusable or not.
    @objc public private(set) var usage: STPSourceUsage = .unknown
    /// Information related to the verification flow. Present if the source's flow
    /// is verification.
    @objc public private(set) var verification: STPSourceVerification?
    /// Information about the source specific to its type
    @objc public private(set) var details: [AnyHashable: Any]?
    /// If this is a card source, this property provides typed access to the
    /// contents of the `details` dictionary.
    @objc public private(set) var cardDetails: STPSourceCardDetails?
    /// If this is a Klarna source, this property provides typed access to the
    /// contents of the `details` dictionary.
    @objc public private(set) var klarnaDetails: STPSourceKlarnaDetails?
    /// If this is a SEPA Debit source, this property provides typed access to the
    /// contents of the `details` dictionary.
    @objc public private(set) var sepaDebitDetails: STPSourceSEPADebitDetails?
    /// If this is a WeChat Pay source, this property provides typed access to the
    /// contents of the `details` dictionary.
    @objc public private(set) var weChatPayDetails: STPSourceWeChatPayDetails?
    // MARK: - Deprecated

    /// A set of key/value pairs associated with the source object.
    /// @deprecated Metadata is no longer returned to clients using publishable keys. Retrieve them on your server using yoursecret key instead.
    /// - seealso: https://stripe.com/docs/api#metadata
    @available(
        *, deprecated,
        message:
            "Metadata is no longer returned to clients using publishable keys. Retrieve them on your server using yoursecret key instead."
    )
    @objc public private(set) var metadata: [String: String]?
    @objc public var stripeID = ""
    @objc private(set) public var allResponseFields: [AnyHashable: Any] = [:]

    // MARK: - STPSourceType
    class func stringToTypeMapping() -> [String: NSNumber] {
        return [
            "bancontact": NSNumber(value: STPSourceType.bancontact.rawValue),
            "card": NSNumber(value: STPSourceType.card.rawValue),
            "giropay": NSNumber(value: STPSourceType.giropay.rawValue),
            "ideal": NSNumber(value: STPSourceType.iDEAL.rawValue),
            "sepa_debit": NSNumber(value: STPSourceType.SEPADebit.rawValue),
            "sofort": NSNumber(value: STPSourceType.sofort.rawValue),
            "three_d_secure": NSNumber(value: STPSourceType.threeDSecure.rawValue),
            "alipay": NSNumber(value: STPSourceType.alipay.rawValue),
            "p24": NSNumber(value: STPSourceType.P24.rawValue),
            "eps": NSNumber(value: STPSourceType.EPS.rawValue),
            "multibanco": NSNumber(value: STPSourceType.multibanco.rawValue),
            "wechat": NSNumber(value: STPSourceType.weChatPay.rawValue),
            "klarna": NSNumber(value: STPSourceType.klarna.rawValue),
        ]
    }

    @objc(typeFromString:)
    class func type(from string: String) -> STPSourceType {
        let key = string.lowercased()
        let typeNumber = self.stringToTypeMapping()[key]

        if let typeNumber = typeNumber {
            return (STPSourceType(rawValue: typeNumber.intValue))!
        }

        return .unknown
    }

    @objc(stringFromType:)
    class func string(from type: STPSourceType) -> String? {
        return
            (self.stringToTypeMapping() as NSDictionary).allKeys(
                for: NSNumber(value: type.rawValue)
            )
            .first as? String
    }

    // MARK: - STPSourceFlow
    class func stringToFlowMapping() -> [String: NSNumber] {
        return [
            "redirect": NSNumber(value: STPSourceFlow.redirect.rawValue),
            "receiver": NSNumber(value: STPSourceFlow.receiver.rawValue),
            "code_verification": NSNumber(value: STPSourceFlow.codeVerification.rawValue),
            "none": NSNumber(value: STPSourceFlow.none.rawValue),
        ]
    }

    @objc(flowFromString:)
    class func flow(from string: String) -> STPSourceFlow {
        let key = string.lowercased()
        let flowNumber = self.stringToFlowMapping()[key]

        if let flowNumber = flowNumber {
            return (STPSourceFlow(rawValue: flowNumber.intValue))!
        }

        return .unknown
    }

    @objc(stringFromFlow:)
    class func string(from flow: STPSourceFlow) -> String? {
        return
            (self.stringToFlowMapping() as NSDictionary).allKeys(
                for: NSNumber(value: flow.rawValue)
            )
            .first as? String
    }

    // MARK: - STPSourceStatus
    class func stringToStatusMapping() -> [String: NSNumber] {
        return [
            "pending": NSNumber(value: STPSourceStatus.pending.rawValue),
            "chargeable": NSNumber(value: STPSourceStatus.chargeable.rawValue),
            "consumed": NSNumber(value: STPSourceStatus.consumed.rawValue),
            "canceled": NSNumber(value: STPSourceStatus.canceled.rawValue),
            "failed": NSNumber(value: STPSourceStatus.failed.rawValue),
        ]
    }

    @objc(statusFromString:)
    class func status(from string: String) -> STPSourceStatus {
        let key = string.lowercased()
        let statusNumber = self.stringToStatusMapping()[key]

        if let statusNumber = statusNumber {
            return (STPSourceStatus(rawValue: statusNumber.intValue))!
        }

        return .unknown
    }

    @objc(stringFromStatus:)
    class func string(from status: STPSourceStatus) -> String? {
        return
            (self.stringToStatusMapping() as NSDictionary).allKeys(
                for: NSNumber(value: status.rawValue)
            )
            .first as? String
    }

    // MARK: - STPSourceUsage
    class func stringToUsageMapping() -> [String: NSNumber] {
        return [
            "reusable": NSNumber(value: STPSourceUsage.reusable.rawValue),
            "single_use": NSNumber(value: STPSourceUsage.singleUse.rawValue),
        ]
    }

    @objc(usageFromString:)
    class func usage(from string: String) -> STPSourceUsage {
        let key = string.lowercased()
        let usageNumber = self.stringToUsageMapping()[key]

        if let usageNumber = usageNumber {
            return (STPSourceUsage(rawValue: usageNumber.intValue))!
        }

        return .unknown
    }

    @objc(stringFromUsage:)
    class func string(from usage: STPSourceUsage) -> String? {
        return
            (self.stringToUsageMapping() as NSDictionary).allKeys(
                for: NSNumber(value: usage.rawValue)
            )
            .first as? String
    }

    // MARK: - Equality
    /// :nodoc:
    @objc
    public override func isEqual(_ object: Any?) -> Bool {
        return isEqual(to: object as? STPSource)
    }

    /// :nodoc:
    @objc public override var hash: Int {
        return stripeID.hash
    }

    func isEqual(to source: STPSource?) -> Bool {
        if self === source {
            return true
        }

        guard let source = source else {
            return false
        }

        return stripeID == source.stripeID
    }

    // MARK: - Description
    /// :nodoc:
    @objc public override var description: String {
        let props = [
            // Object
            String(format: "%@: %p", NSStringFromClass(STPSource.self), self),
            // Identifier
            "stripeID = \(stripeID)",
            // Source details (alphabetical)
            "amount = \(amount ?? 0)",
            "clientSecret = \(((clientSecret) != nil ? "<redacted>" : nil) ?? "")",
            "created = \(String(describing: created))",
            "currency = \(currency ?? "")",
            "flow = \((STPSource.string(from: flow)) ?? "unknown")",
            "livemode = \((livemode) ? "YES" : "NO")",
            "owner = \(((owner) != nil ? "<redacted>" : nil) ?? "")",
            "receiver = \(String(describing: receiver))",
            "redirect = \(String(describing: redirect))",
            "status = \((STPSource.string(from: status)) ?? "unknown")",
            "type = \((STPSource.string(from: type)) ?? "unknown")",
            "usage = \((STPSource.string(from: usage)) ?? "unknown")",
            "verification = \(String(describing: verification))",
        ]

        return "<\(props.joined(separator: "; "))>"
    }

    // MARK: - STPAPIResponseDecodable
    @objc func stripeObject() -> String {
        return "source"
    }

    @objc
    public class func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? {
        guard let response = response else {
            return nil
        }
        let dict = (response as NSDictionary).stp_dictionaryByRemovingNulls() as NSDictionary

        // required fields
        let stripeId = dict.stp_string(forKey: "id")
        let rawStatus = dict.stp_string(forKey: "status")
        let rawType = dict.stp_string(forKey: "type")
        if stripeId == nil || rawStatus == nil || rawType == nil || dict["livemode"] == nil {
            return nil
        }

        let source = self.init()
        source.stripeID = stripeId ?? ""
        source.amount = dict.stp_number(forKey: "amount")
        source.clientSecret = dict.stp_string(forKey: "client_secret")
        source.created = dict.stp_date(forKey: "created")
        source.currency = dict.stp_string(forKey: "currency")
        let rawFlow = dict.stp_string(forKey: "flow")
        source.flow = self.flow(from: rawFlow ?? "")
        source.livemode = dict.stp_bool(forKey: "livemode", or: true)
        let rawOwner = dict.stp_dictionary(forKey: "owner")
        source.owner = STPSourceOwner.decodedObject(fromAPIResponse: rawOwner)
        let rawReceiver = dict.stp_dictionary(forKey: "receiver")
        source.receiver = STPSourceReceiver.decodedObject(fromAPIResponse: rawReceiver)
        let rawRedirect = dict.stp_dictionary(forKey: "redirect")
        source.redirect = STPSourceRedirect.decodedObject(fromAPIResponse: rawRedirect)
        source.status = self.status(from: rawStatus ?? "")
        source.type = self.type(from: rawType ?? "")
        let rawUsage = dict.stp_string(forKey: "usage")
        source.usage = self.usage(from: rawUsage ?? "")
        let rawVerification = dict.stp_dictionary(forKey: "verification")
        if let rawVerification = rawVerification {
            source.verification = STPSourceVerification.decodedObject(
                fromAPIResponse: rawVerification)
        }
        source.details = dict.stp_dictionary(forKey: rawType ?? "")
        source.allResponseFields = dict as! [AnyHashable: Any]

        if source.type == .card {
            if let details1 = source.details {
                source.cardDetails = STPSourceCardDetails.decodedObject(fromAPIResponse: details1)
            }
        } else if source.type == .SEPADebit {
            source.sepaDebitDetails = STPSourceSEPADebitDetails.decodedObject(
                fromAPIResponse: source.details)
        } else if source.type == .weChatPay {
            source.weChatPayDetails = STPSourceWeChatPayDetails.decodedObject(
                fromAPIResponse: source.details)
        } else if source.type == .klarna {
            source.klarnaDetails = STPSourceKlarnaDetails.decodedObject(
                fromAPIResponse: source.details)
        }

        return source
    }

    // MARK: - STPPaymentOption
    @objc public var image: UIImage {
        if type == .card, let cardDetails = cardDetails {
            return STPImageLibrary.cardBrandImage(for: cardDetails.brand)
        } else {
            return STPImageLibrary.cardBrandImage(for: .unknown)
        }
    }

    @objc public var templateImage: UIImage {
        if type == .card, let cardDetails = cardDetails {
            return STPImageLibrary.templatedBrandImage(for: cardDetails.brand)
        } else {
            return STPImageLibrary.templatedBrandImage(for: .unknown)
        }
    }

    @objc public var label: String {
        switch type {
        case .bancontact:
            return STPLocalizedString("Bancontact", "Source type brand name")
        case .card:
            if let cardDetails = cardDetails {
                let brand = STPCard.string(from: cardDetails.brand)
                return "\(brand) \(cardDetails.last4 ?? "")"
            } else {
                return STPCard.string(from: .unknown)
            }
        case .giropay:
            return STPLocalizedString("Giropay", "Source type brand name")
        case .iDEAL:
            return STPLocalizedString("iDEAL", "Source type brand name")
        case .SEPADebit:
            return STPLocalizedString("SEPA Direct Debit", "Source type brand name")
        case .sofort:
            return STPLocalizedString("Sofort", "Source type brand name")
        case .threeDSecure:
            return STPLocalizedString("3D Secure", "Source type brand name")
        case .alipay:
            return STPLocalizedString("Alipay", "Source type brand name")
        case .P24:
            return STPLocalizedString("P24", "Source type brand name")
        case .EPS:
            return STPLocalizedString("EPS", "Source type brand name")
        case .multibanco:
            return STPLocalizedString("Multibanco", "Source type brand name")
        case .weChatPay:
            return STPLocalizedString("WeChat Pay", "Source type brand name")
        case .klarna:
            return STPLocalizedString("Klarna", "Source type brand name")
        case .unknown:
            return STPLocalizedString("Unknown", "Default missing source type label")
        @unknown default:
            return STPLocalizedString("Unknown", "Default missing source type label")
        }
    }

    @objc public var isReusable: Bool {
        return usage != .singleUse
    }
}
