//
//  VPNSubscription.swift
//  Lockdown
//
//  Copyright © 2018 Confirmed, Inc. All rights reserved.
//

import UIKit
import SwiftyStoreKit
import PromiseKit
import CocoaLumberjackSwift
import StoreKit

struct OneTimeProducts: ToList {
    let weekly: String
    let weeklyTrial: String
    let yearly: String
    let yearlyTrial: String
}

struct SpecialOfferProducts: ToList {
    let yearly: String
}

struct FeedbackProducts: ToList {
    let weekly: String
    let yearly: String
}

protocol ToList {
    func toList() -> [String]
}

extension ToList {
    func toList() -> [String] {
        let otherSelf = Mirror(reflecting: self)
        return otherSelf.children.compactMap {
            $0.value as? String
        }
    }
}

struct InternalSubscription: Hashable {
    let productId: String
    let period: SKProduct.PeriodUnit
    let trialDuration: String?
    let priceLocale: Locale
    let price: NSDecimalNumber
    let offer: NSDecimalNumber?
    
    static var mockWeekly: InternalSubscription {
        InternalSubscription(productId: "lockdown.weekly.1.202408.no_trial.4hrs_offer", period: .week, trialDuration: nil, priceLocale: .current, price: 0.99, offer: nil)
    }
    
    static var mockWeeklyTrial: InternalSubscription {
        InternalSubscription(productId: "lockdown.weekly.1.202408.3_days_free_trial.4hrs_offer", period: .week, trialDuration: "3 days", priceLocale: .current, price: 0.99, offer: nil)
    }
    
    static var mockYearly: InternalSubscription {
        InternalSubscription(productId: "lockdown.yearly.40.202408.no_trial.4hrs_offer_", period: .year, trialDuration: nil, priceLocale: .current, price: 39.99, offer: nil)
    }
    
    static var mockYearlTrial: InternalSubscription {
        InternalSubscription(productId: "lockdown.yearly.40.202408.3_days_free_trial.4hrs_offer", period: .year, trialDuration: "3 days", priceLocale: .current, price: 39.99, offer: nil)
    }
    
    static var mockYearlyBF: InternalSubscription {
        InternalSubscription(productId: "lockdown.yearly.30.202412.1_year_no_trial.4h_screen_holiday", period: .year, trialDuration: nil, priceLocale: .current, price: 99.99, offer: 29.99)
    }
}

enum SubscriptionState: Int {
    case Uninitialized = 1, Subscribed, NotSubscribed
}

actor VPNSubscription: NSObject {
    
    enum SubscriptionType {
        case oneTime
        case feedback
        case specialOffer
        
        var productIds: [String] {
            switch self {
            case .oneTime: VPNSubscription.oneTimeProducts.toList()
            case .feedback: VPNSubscription.feedbackProducts.toList()
            case .specialOffer: VPNSubscription.specialOfferProducts.toList()
            }
        }
    }
    
    private var cachedSubscriptions: [SubscriptionType: [InternalSubscription]] = [:]
    
    static var shared = VPNSubscription()
    
    static let productIdAdvancedMonthly = "LockdowniOSFirewallMonthly"
    static let productIdAdvancedYearly = "LockdowniOSFirewallAnnual"
    static let productIdMonthly = "LockdowniOSVpnMonthly"
    static let productIdAnnual = "LockdowniOSVpnAnnual"
    static let productIdMonthlyPro = "LockdowniOSVpnMonthlyPro"
    static let productIdAnnualPro = "LockdowniOSVpnAnnualPro"
    static let productIds: Set = [productIdAdvancedMonthly, productIdAdvancedYearly, productIdMonthly, productIdAnnual, productIdMonthlyPro, productIdAnnualPro]
    static let oneTimeProducts = OneTimeProducts(weekly: "lockdown.weekly.1.202408.no_trial.4hrs_offer",
                                                  weeklyTrial: "lockdown.weekly.1.202408.3_days_free_trial.4hrs_offer",
                                                  yearly: "lockdown.yearly.40.202408.no_trial.4hrs_offer_", 
                                                  yearlyTrial: "lockdown.yearly.40.202408.3_days_free_trial.4hrs_offer")
    static let feedbackProducts = FeedbackProducts(weekly: "lockdown.weekly.1.202409.no_trial.feedback",
                                                   yearly: "lockdown.yearly.40.202409.no_trial.feedback")
    static let specialOfferProducts = SpecialOfferProducts(yearly: "lockdown.yearly.30.202412.1_year_no_trial.4h_screen_holiday")
    static var selectedProductId = productIdAdvancedMonthly

    // Advanced Level
    static var defaultPriceStringAdvancedMonthly = "$4.99"
    static var defaultPriceStringAdvancedYearly = "$29.99"
    static var defaultPriceSubStringAdvancedYearly = "$2.49"
    static var defaultUpgradePriceStringAdvancedMonthly = "$4.99"
    static var defaultUpgradePriceStringAdvancedYearly = "$29.99"
    
    // Anonymous Level
    static var defaultPriceStringMonthly = "$8.99"
    static var defaultPriceStringAnnual = "$59.99"
    static var defaultPriceSubStringAnnual = "$4.99"
    static var defaultUpgradePriceStringAnnual = "$59.99"
    static var defaultUpgradePriceStringMonthly = "$8.99"
    
    
    // Universal Level
    static var defaultPriceStringMonthlyPro = "$11.99"
    static var defaultPriceStringAnnualPro = "$99.99"
    static var defaultPriceStringAnnualPro70Off = "$29.99"
    static var defaultPriceSubStringAnnualPro = "$8.33"
    static var defaultUpgradePriceStringAnnualPro = "$99.99"
    static var defaultUpgradePriceStringMonthlyPro = "$11.99"
    
    @discardableResult
    func loadSubscriptions(type: SubscriptionType) async -> [InternalSubscription]? {
        if let subscriptions = cachedSubscriptions[type], !subscriptions.isEmpty {
            return subscriptions
        }
        cachedSubscriptions[type] = await _loadSubscriptions(productIds: Set(type.productIds))
        return cachedSubscriptions[type]
    }
    
    private func _loadSubscriptions(productIds: Set<String>) async -> [InternalSubscription]? {
        DDLogInfo("cache localized price for productIds: \(productIds)")
        let currencyFormatter = NumberFormatter()
        currencyFormatter.usesGroupingSeparator = true
        currencyFormatter.numberStyle = .currency
        
        return await withCheckedContinuation { continuation in
            SwiftyStoreKit.retrieveProductsInfo(productIds) { result in
                DDLogInfo("retrieve products results: \(result)")
                var subs = [InternalSubscription]()
                for product in result.retrievedProducts {
                    
                    let period = Self.subscriptionPeriod(product: product)
                    let trialDuration = Self.trialDuraion(for: product.introductoryPrice)
                    currencyFormatter.locale = product.priceLocale
                    if let period {
                        let ip = InternalSubscription(productId: product.productIdentifier,
                                                      period: period,
                                                      trialDuration: trialDuration,
                                                      priceLocale: product.priceLocale,
                                                      price: product.price,
                                                      offer: product.price)
                        subs.append(ip)
                    }
                }
                continuation.resume(returning: subs)
            }
        }
    }
    private override init() {
        super.init()
    }
    
    static func purchase(succeeded: @escaping () -> Void, errored: @escaping (Error) -> Void) {
        DDLogInfo("purchase")
        SwiftyStoreKit.purchaseProduct(selectedProductId, atomically: true) { result in
            switch result {
                case .success:
                    firstly {
                        try Client.signIn()
                    }
                    .then { (signin: SignIn) -> Promise<GetKey> in
                        try Client.getKey()
                    }
                    .done { (getKey: GetKey) in
                        try setVPNCredentials(id: getKey.id, keyBase64: getKey.b64)
                        BaseUserService.shared.user.resetCache()
                        succeeded()
                    }
                    .catch { error in
                        errored(error)
                    }
                case .error(let error):
                    DDLogError("purchase error: \(error)")
                    errored(error)
            }
        }
    }
    
    static func subscriptionPeriod(product: SKProduct) -> SKProduct.PeriodUnit? {
        guard let subscriptionPeriod = product.subscriptionPeriod else {
            return nil
        }

        return subscriptionPeriod.unit
    }
    
    static func setTrialDuration(productId: String, duration: String?) {
        let trialKey = productId + "Trial"
        
        guard let duration else {
            UserDefaults.standard.removeObject(forKey: trialKey)
            DDLogInfo("Unavaible trial for \(productId)")
            return
        }
        DDLogInfo("Setting trial a duration \(duration) for \(productId)")
        UserDefaults.standard.set(duration, forKey: trialKey)
    }
    
    static func setProductIdPrice(productId: String, price: String) {
        DDLogInfo("Setting product id price \(price) for \(productId)")
        UserDefaults.standard.set(price, forKey: productId + "Price")
    }
    
    static func setProductIdUpgradePrice(productId: String, upgradePrice: String) {
        DDLogInfo("Setting product id upgrade price \(upgradePrice) for \(productId)")
        UserDefaults.standard.set(upgradePrice, forKey: productId + "UpgradePrice")
    }
    
    static func setProductIdPriceAnnualMonthly(productId: String, price: String) {
        DDLogInfo("Setting product id price yearly per month \(price) for \(productId)")
        UserDefaults.standard.set(price, forKey: productId + "MonthlyPrice")
    }
    
    static func setProductIdUpgradePriceAnnualMonthly(productId: String, price: String) {
        DDLogInfo("Setting product id upgrade price yearly per month\(price) for \(productId)")
        UserDefaults.standard.set(price, forKey: productId + "MonthlyUpgradePrice")
    }
    
    enum SubscriptionContext {
        case new
        case upgrade
        case monthlyNew
        case monthlyUpgrade
    }
    
    static func getProductIdPrice(productId: String, for context: SubscriptionContext) -> String {
        switch context {
        case .new:
            return getProductIdPrice(productId: productId)
        case .upgrade:
            return getProductIdUpgradePrice(productId: productId)
        case .monthlyNew:
            return getProductIdPrice(productId: productId)
        case .monthlyUpgrade:
            return getProductIdUpgradePrice(productId: productId)
        }
    }
    
    static func getProductIdPriceMonthly(productId: String) -> String {
        DDLogInfo("Getting product id price yearly per month \(productId)")
        if let price = UserDefaults.standard.string(forKey: productId + "MonthlyPrice") {
            DDLogInfo("Got product id price yearly per month for \(productId): \(price)")
            return price
        }
        else {
            DDLogError("Found no cached price yearly per month for productId \(productId), returning default")
            switch productId {
            case productIdAdvancedYearly:
                return defaultPriceSubStringAdvancedYearly
            case productIdAnnual:
                return defaultPriceSubStringAnnual
            case productIdAnnualPro:
                return defaultPriceSubStringAnnualPro
            default:
                DDLogError("Invalid product Id: \(productId)")
                return "Invalid Price"
            }
        }
    }
    
    static func trialDuration(productId: String) -> String? {
        UserDefaults.standard.string(forKey: productId + "Trial")
    }
    
    static func getProductIdPrice(productId: String) -> String {
        DDLogInfo("Getting product id price for \(productId)")
        if let price = UserDefaults.standard.string(forKey: productId + "Price") {
            DDLogInfo("Got product id price for \(productId): \(price)")
            return price
        }
        else {
            DDLogError("Found no cached price for productId \(productId), returning default")
            switch productId {
            case productIdAdvancedMonthly:
                return defaultPriceStringAdvancedMonthly
            case productIdAdvancedYearly:
                return defaultPriceStringAdvancedYearly
            case productIdMonthly:
                return defaultPriceStringMonthly
            case productIdMonthlyPro:
                return defaultPriceStringMonthlyPro
            case productIdAnnual:
                return defaultPriceStringAnnual
            case productIdAnnualPro:
                return defaultPriceStringAnnualPro
            default:
                DDLogError("Invalid product Id: \(productId)")
                return "Invalid Price"
            }
        }
    }
    
    static func getProductIdUpgradePrice(productId: String) -> String {
        DDLogInfo("Getting product id upgrade price for \(productId)")
        if let upgradePrice = UserDefaults.standard.string(forKey: productId + "UpgradePrice") {
            DDLogInfo("Got product id upgrade price for \(productId): \(upgradePrice)")
            return upgradePrice
        }
        else {
            DDLogError("Found no cached upgrade price for productId \(productId), returning default")
            switch productId {
            case productIdAdvancedMonthly:
                return defaultUpgradePriceStringAdvancedMonthly
            case productIdAdvancedYearly:
                return defaultUpgradePriceStringAdvancedYearly
            case productIdMonthly:
                return defaultUpgradePriceStringMonthly
            case productIdMonthlyPro:
                return defaultUpgradePriceStringMonthlyPro
            case productIdAnnual:
                return defaultUpgradePriceStringAnnual
            case productIdAnnualPro:
                return defaultUpgradePriceStringAnnualPro
            default:
                DDLogError("Invalid product Id: \(productId)")
                return "Invalid Upgrade Price"
            }
        }
    }
    
    static func cacheLocalizedPrices() -> Void {

        let currencyFormatter = NumberFormatter()
        currencyFormatter.usesGroupingSeparator = true
        currencyFormatter.numberStyle = .currency
        
        DDLogInfo("cache localized price for productIds: \(productIds)")
        SwiftyStoreKit.retrieveProductsInfo(productIds) { result in
            DDLogInfo("retrieve products results: \(result)")
            for product in result.retrievedProducts {
                DDLogInfo("product locale: \(product.priceLocale)")
                DDLogInfo("productprice: \(product.localizedPrice ?? "n/a")")
                
                if product.productIdentifier == productIdAdvancedMonthly {
                    if product.localizedPrice != nil {
                        DDLogInfo("setting productIdAdvancedMonthly display price = " + product.localizedPrice!)
                        setProductIdPrice(productId: productIdAdvancedMonthly, price: "\(product.localizedPrice!)")
                        setProductIdUpgradePrice(productId: productIdAdvancedMonthly, upgradePrice: "\(product.localizedPrice!)")
                    }
                    else {
                        DDLogError("monthly nil localizedPrice, setting default")
                        setProductIdPrice(productId: productIdAdvancedMonthly, price: defaultPriceStringAdvancedMonthly)
                        setProductIdUpgradePrice(productId: productIdAdvancedMonthly, upgradePrice: defaultUpgradePriceStringAdvancedMonthly)
                    }
                }
                else if product.productIdentifier == productIdAdvancedYearly {
                    if product.localizedPrice != nil {
                        currencyFormatter.locale = product.priceLocale
                        let priceMonthly = product.price.dividing(by: 12)
                        if let priceString = currencyFormatter.string(from: priceMonthly) {
                            setProductIdPriceAnnualMonthly(productId: productIdAdvancedYearly, price: priceString)
                            DDLogInfo("setting productIdAdvancedAnnualMonthly display price = " + priceString)
                        }
                        DDLogInfo("setting productIdAdvancedYearly display price = " + product.localizedPrice!)
                        setProductIdPrice(productId: productIdAdvancedYearly, price: "\(product.localizedPrice!)")
                        setProductIdUpgradePrice(productId: productIdAdvancedYearly, upgradePrice: "\(product.localizedPrice!)")
                    }
                    else {
                        DDLogError("monthly nil localizedPrice, setting default")
                        setProductIdPrice(productId: productIdAdvancedYearly, price: defaultPriceStringAdvancedYearly)
                        setProductIdUpgradePrice(productId: productIdAdvancedYearly, upgradePrice: defaultUpgradePriceStringAdvancedYearly)
                        setProductIdPriceAnnualMonthly(productId: productIdAdvancedYearly, price: defaultPriceSubStringAdvancedYearly)
                    }
                }
                else if product.productIdentifier == productIdAnnual {
                    if product.localizedPrice != nil {
                        currencyFormatter.locale = product.priceLocale
                        let priceMonthly = product.price.dividing(by: 12)
                        if let priceString = currencyFormatter.string(from: priceMonthly) {
                            setProductIdPriceAnnualMonthly(productId: productIdAnnual, price: priceString)
                            DDLogInfo("setting productIdAnnualAnnualMonthly display price = " + priceString)
                        }
                        DDLogInfo("setting productIdAnnualAnnual display price = annual product price / 12 = " + product.localizedPrice!)
                        setProductIdPrice(productId: productIdAnnual, price: "\(product.localizedPrice!)")
                        setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: "\(product.localizedPrice!)")
                    }
                    
                    else {
                        DDLogError("unable to format price with currencyformatter: " + product.price.stringValue)
                        setProductIdPrice(productId: productIdAnnual, price: defaultPriceStringAnnual)
                        setProductIdUpgradePrice(productId: productIdAnnual, upgradePrice: defaultUpgradePriceStringAnnual)
                        setProductIdPriceAnnualMonthly(productId: productIdAnnual, price: defaultPriceSubStringAnnual)
                    }
                }
                else if product.productIdentifier == productIdMonthly {
                    if product.localizedPrice != nil {
                        DDLogInfo("setting productIdMonthly display price = " + product.localizedPrice!)
                        setProductIdPrice(productId: productIdMonthly, price: "\(product.localizedPrice!)")
                        setProductIdUpgradePrice(productId: productIdMonthly, upgradePrice: "\(product.localizedPrice!)")
                    }
                    else {
                        DDLogError("monthly nil localizedPrice, setting default")
                        setProductIdPrice(productId: productIdMonthly, price: defaultPriceStringMonthly)
                        setProductIdUpgradePrice(productId: productIdMonthly, upgradePrice: defaultUpgradePriceStringMonthly)
                    }
                }
                else if product.productIdentifier == productIdMonthlyPro {
                    if product.localizedPrice != nil {
                        DDLogInfo("setting productIdMonthlyPro display price = " + product.localizedPrice!)
                        setProductIdPrice(productId: productIdMonthlyPro, price: "\(product.localizedPrice!)")
                        setProductIdUpgradePrice(productId: productIdMonthlyPro, upgradePrice: "\(product.localizedPrice!)")
                    }
                    else {
                        DDLogError("monthlyPro nil localizedPrice, setting default")
                        setProductIdPrice(productId: productIdMonthlyPro, price: defaultPriceStringMonthlyPro)
                        setProductIdUpgradePrice(productId: productIdMonthlyPro, upgradePrice: defaultUpgradePriceStringMonthlyPro)
                    }
                }
                else if product.productIdentifier == productIdAnnualPro {
                    if product.localizedPrice != nil {
                        currencyFormatter.locale = product.priceLocale
                        let priceMonthly = product.price.dividing(by: 12)
                        if let priceString = currencyFormatter.string(from: priceMonthly) {
                            DDLogInfo("setting productIdAnnualPro display price = annualPro product price / 12 = " + priceString)
                            setProductIdPriceAnnualMonthly(productId: productIdAnnualPro, price: priceString)
                        }
                        DDLogInfo("setting productIdAnnualPro display price = " + product.localizedPrice!)
                        setProductIdPrice(productId: productIdAnnualPro, price: "\(product.localizedPrice!)")
                        setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: "\(product.localizedPrice!)")
                    }
                    else {
                        DDLogError("unable to format price with currencyformatter: " + product.price.stringValue)
                        setProductIdPrice(productId: productIdAnnualPro, price: defaultPriceStringAnnualPro)
                        setProductIdUpgradePrice(productId: productIdAnnualPro, upgradePrice: defaultUpgradePriceStringAnnualPro)
                        setProductIdPriceAnnualMonthly(productId: productIdAnnualPro, price: defaultPriceSubStringAnnualPro)
                    }
                }
                setTrialDuration(productId: product.productIdentifier, duration: trialDuraion(for: product.introductoryPrice))
            }
            for invalidProductId in result.invalidProductIDs {
                DDLogError("invalid product id: \(invalidProductId)");
            }
        }
    }
    
    private static func trialDuraion(for trial: SKProductDiscount?) -> String? {
        guard let trial,
              trial.paymentMode == .freeTrial else {
            return nil
        }
        let unit = switch trial.subscriptionPeriod.unit {
        case .day: NSLocalizedString("day", comment: "day")
        case .week: NSLocalizedString("week", comment: "week")
        case .month: NSLocalizedString("month", comment: "month")
        case .year: NSLocalizedString("year", comment: "year")
        }
        return "\(trial.subscriptionPeriod.numberOfUnits)" + "-" + unit
    }
}

extension Subscription.PlanType {
    var productId: String? {
        switch self {
        case .advancedMonthly:
            return VPNSubscription.productIdAdvancedMonthly
        case .advancedAnnual:
            return VPNSubscription.productIdAdvancedYearly
        case .anonymousMonthly:
            return VPNSubscription.productIdMonthly
        case .anonymousAnnual:
            return VPNSubscription.productIdAnnual
        case .universalMonthly:
            return VPNSubscription.productIdMonthlyPro
        case .universalAnnual:
            return VPNSubscription.productIdAnnualPro
        default:
            return nil
        }
    }
    
    static var supported: [Subscription.PlanType] {
        return [.advancedMonthly, .advancedAnnual, .anonymousMonthly, .anonymousAnnual, .universalMonthly, .universalAnnual]
    }
    
    var availableUpgrades: [Subscription.PlanType]? {
        switch self {
        case .advancedMonthly:
            return [.advancedAnnual, .anonymousMonthly, .anonymousAnnual, .universalMonthly, .universalAnnual]
        case .advancedAnnual:
            return [.anonymousMonthly, .anonymousAnnual, .universalMonthly, .universalAnnual]
        case .anonymousMonthly:
            return [.anonymousAnnual, .universalMonthly, .universalAnnual]
        case .anonymousAnnual:
            return [.universalMonthly, .universalAnnual]
        case .universalMonthly:
            return [.universalAnnual]
        case .universalAnnual:
            return []
        default:
            return nil
        }
    }
    
    var unavailableToUpgrade: [Subscription.PlanType]? {
        guard let upgrades = availableUpgrades else {
            return nil
        }
        
        var candidates = Subscription.PlanType.supported
        candidates.removeAll(where: { upgrades.contains($0) })
        return candidates
    }
    
    func canUpgrade(to newPlan: Subscription.PlanType) -> Bool {
        return availableUpgrades?.contains(newPlan) == true
    }
}
