import APIClient
import BackendEnvironmentClient
import ComposableArchitecture
import Foundation
import InstanceIDClient
import RevenueCat
import Utilities

public struct SubscriptionsConfiguration: Hashable, Identifiable {
    public static func == (lhs: SubscriptionsConfiguration, rhs: SubscriptionsConfiguration) -> Bool {
        lhs.subscriptions == rhs.subscriptions
    }

    public let id = UUID()
    public let subscriptions: [Subscription]
    public let currencyCode: String?
}

public struct Subscription: Hashable, Identifiable {
    public enum Period {
        case day, week, month, year
    }

    public enum SemanticPlan: Int {
        case basic, pro, premier
    }

    public var id: String
    public var title: String
    public var period: Period
    public var price: Double
    public var yearPrice: Double?
    public var discountPrice: Double?
    public var discountYearPrice: Double?
    public var currencyCode: String?
    public var info: [String]
    public var promotionalOffer: PromotionalOffer?

    public var semanticPlan: SemanticPlan {
        if id.contains("premier") {
            return .premier
        } else if id.contains("pro") {
            return .pro
        } else if id.contains("basic") {
            return .basic
        } else {
            log.telemetry.assertionFailure("unknown plan \(id)")
            return .basic
        }
    }
}

public struct Entitlement: Equatable, Comparable {
    public let period: Subscription.Period
    public let plan: Subscription.SemanticPlan
    public let planIdentifier: String
    public let isLegacy: Bool
    public let unsubscribed: Bool

    public init(period: Subscription.Period,
                plan: Subscription.SemanticPlan,
                planIdentifier: String,
                isLegacy: Bool,
                unsubscribed: Bool) {
        self.period = period
        self.plan = plan
        self.planIdentifier = planIdentifier
        self.isLegacy = isLegacy
        self.unsubscribed = unsubscribed
    }

    public static func < (lhs: Entitlement, rhs: Entitlement) -> Bool {
        // First compare by plan level (basic < pro < premier)
        if lhs.plan.rawValue != rhs.plan.rawValue {
            return lhs.plan.rawValue < rhs.plan.rawValue
        }
        // Then compare by period (month < year)
        let lhsPeriodOrder: Int = {
            switch lhs.period {
            case .day: return 0
            case .week: return 1
            case .month: return 2
            case .year: return 3
            }
        }()
        let rhsPeriodOrder: Int = {
            switch rhs.period {
            case .day: return 0
            case .week: return 1
            case .month: return 2
            case .year: return 3
            }
        }()
        return lhsPeriodOrder < rhsPeriodOrder
    }
}

public struct CreditsPackage: Equatable, Hashable, Identifiable {
    public var id: String
    public var description: String
    public var title: String
    public var price: String

    public static func == (lhs: CreditsPackage, rhs: CreditsPackage) -> Bool {
        return lhs.id == rhs.id
    }
}

@DependencyClient
public struct PaywallClient {
    public var configure: () -> Void
    public var updateUser: (_ user: User) async -> Void
    public var subscriptions: (SubscriptionInfoResponse?) async throws -> SubscriptionsConfiguration
    public var creditsPackages: () async throws -> [CreditsPackage]
    public var purchase: (Subscription, Bool) async throws -> [Entitlement]
    public var purchaseCreditsPackage: (CreditsPackage) async throws -> Void
    public var restore: () async throws -> [Entitlement]
    public var syncPurchases: () async throws -> [Entitlement]
    public var entitlement: () async throws -> [Entitlement]
    /// Links `idfv` to RevenueCat, or `idfa` once we start asking for ATT permissioning
    /// Requred for RevenueCat to successfully send events to our integrations like Branch/AppsFlyer/Singular
    public var collectDeviceIdentifiers: () -> Void
    public var loadSubscriptionPage: () async throws -> SubscriptionPageResponse?
}

public extension PaywallClient {
    enum User: Equatable {
        case authenticated(userId: String)
        case anonymous

        public var userId: String? {
            switch self {
            case .authenticated(userId: let id): id
            case .anonymous: nil
            }
        }
    }
}

public enum PaywallError: LocalizedError {
    case noProducts
    case productNotFound
    case configurationIssue
    case subscriptionPageNotLoaded
    case cancelled

    public var errorDescription: String? {
        switch self {
        case .noProducts: "Could not fetch products"
        case .productNotFound: "Product not found"
        case .configurationIssue: "RevenueCat configuration issue"
        case .subscriptionPageNotLoaded: "Subscription page could not be loaded"
        case .cancelled: "Purchase was cancelled"
        }
    }
}

extension PaywallClient: DependencyKey {
    public static var liveValue: PaywallClient {
        guard let info = Bundle.main.infoDictionary,
              let apiKey = info["PAYWALL_APIKEY"] as? String,
              !apiKey.isEmpty else {
            fatalError("PAYWALL_APIKEY is not defined in Info.plist")
        }

        @Dependency(\.instanceIdClient) var instanceIdClient
        @Dependency(APIClientV2.self) var apiClient

        return .init(
            configure: {
                Purchases.logLevel = .error
                Purchases.configure(with: .init(withAPIKey: apiKey))
                /// Call during configuration time
                /// This can be called multiple times without issue, and should be called when ATT permissioning changes
                Purchases.shared.attribution.collectDeviceIdentifiers()
            },
            updateUser: { user in
                let userId = switch user {
                case .authenticated(userId: let id): id
                case .anonymous: instanceIdClient.instanceId()
                }

                do {
                    _ = try await Purchases.shared.logIn(userId)
                } catch {
                    log.telemetry.error(error)
                }
            },
            subscriptions: { billingInfo in
                let allOfferings = try await Purchases.shared.offerings()

                if billingInfo == nil {
                    log.telemetry.assertionFailure("tried to fetch subscriptions without billing info loaded")
                    /// Should we request billing info from the API here before we continue?
                }

                let offerings = if let revcatOfferingID = billingInfo?.revcatSubscriptionsOfferingId {
                    if let experimentOffering = allOfferings.offering(identifier: revcatOfferingID) {
                        experimentOffering
                    } else {
                        /// in the case where the backend has given us an offeringID that revcat doesn't know about, default to the current offering instead of showing an empty list of subscriptions
                        allOfferings.current
                    }
                } else {
                    allOfferings.current
                }

                guard let offerings else {
                    return SubscriptionsConfiguration(subscriptions: [], currencyCode: nil)
                }

                struct Info: Decodable {
                    var info: [String]
                    var discountInfo: String?
                    var planKey: String?
                }

                let subscriptions = try await withThrowingTaskGroup(of: Subscription?.self) { group in
                    for package in offerings.availablePackages {
                        group.addTask {
                            let info: Info? = offerings.getMetadataValue(for: package.identifier)
                            
                            guard let periodUnit = package.storeProduct.subscriptionPeriod?.unit else {
                                return nil
                            }
                            
                            let period = Subscription.Period(periodUnit)

                            // MARK: Regular prices
                            let price: Double = package.storeProduct.pricePerMonth?.doubleValue ?? NSDecimalNumber(decimal: package.storeProduct.price).doubleValue

                            var yearPrice: Double?
                            if period == .year {
                                yearPrice = package.storeProduct.pricePerYear?.doubleValue
                            }

                            // MARK: Offers
                            
                            let introductoryOffer = package.storeProduct.introductoryDiscount

                            var promotionalOffer: PromotionalOffer?
                            let promotionalOffers = await package.storeProduct.eligiblePromotionalOffers()
                            let isEligibleforIntroductoryOffer = await Purchases.shared.checkTrialOrIntroDiscountEligibility(product: package.storeProduct).isEligible

                            var discountPricePerMonth: NSDecimalNumber?
                            var discountPricePerYear: NSDecimalNumber?

                            if isEligibleforIntroductoryOffer, let introductoryOffer {
                                discountPricePerMonth = introductoryOffer.pricePerMonth
                                discountPricePerYear = introductoryOffer.pricePerYear
                            } else if let eligibilePromotionalOffer = promotionalOffers.first {
                                discountPricePerMonth = eligibilePromotionalOffer.discount.pricePerMonth
                                discountPricePerYear = eligibilePromotionalOffer.discount.pricePerYear
                                promotionalOffer = eligibilePromotionalOffer
                            }
                            
                            // MARK: Discount prices
                            
                            var discountPrice: Double?
                            if let discountPricePerMonth {
                                discountPrice = discountPricePerMonth.doubleValue
                            }

                            var discountYearPrice: Double?
                            if let discountPricePerYear, period == .year {
                                discountYearPrice = discountPricePerYear.doubleValue
                            }

                            return Subscription(
                                id: package.id,
                                title: package.id.components(separatedBy: ".")
                                    .joined(separator: " ")
                                    .capitalized(with: .autoupdatingCurrent),
                                period: period,
                                price: price,
                                yearPrice: yearPrice,
                                discountPrice: discountPrice,
                                discountYearPrice: discountYearPrice,
                                currencyCode: package.storeProduct.currencyCode,
                                info: info?.info ?? [],
                                promotionalOffer: promotionalOffer
                            )
                        }
                    }

                    var subscriptions: [Subscription] = []
                    for try await subscription in group {
                        if let subscription {
                            subscriptions.append(subscription)
                        }
                    }
                    return subscriptions
                }

                let currencyCode = offerings.availablePackages.first?.storeProduct.currencyCode ?? "USD"

                return SubscriptionsConfiguration(subscriptions: subscriptions, currencyCode: currencyCode)
            },
            creditsPackages: {
                let offerings = try await Purchases.shared.offerings()
                guard let creditsOffering = offerings["credits"] else { throw PaywallError.noProducts }
                let availablePackages = creditsOffering.availablePackages
                guard !availablePackages.isEmpty else { throw PaywallError.noProducts }
                return availablePackages.map { package in
                    .init(
                        id: package.id,
                        description: package.storeProduct.description,
                        title: package.storeProduct.localizedTitle,
                        price: package.storeProduct.localizedPriceString.replacingOccurrences(of: ".00", with: "")
                    )
                }
            },
            purchase: { subscription, upsellAvailable in
                let allOfferings = try await Purchases.shared.offerings().all
                let availablePackages: [RevenueCat.Package] = allOfferings.values.reduce([RevenueCat.Package]()) { acc, cur in acc + cur.availablePackages }
                guard let package = availablePackages.first(where: { $0.id == subscription.id }) else {
                    throw PaywallError.productNotFound
                }

                var result: PurchaseResultData
                if let promotionalOffer = subscription.promotionalOffer, upsellAvailable {
                    result = try await Purchases.shared.purchase(package: package, promotionalOffer: promotionalOffer)
                } else {
                    result = try await Purchases.shared.purchase(package: package)
                }

                if result.userCancelled { throw PaywallError.cancelled }

                return try await result.customerInfo.sunoEntitlements()
            },
            purchaseCreditsPackage: { creditsPackage in
                let offerings = try await Purchases.shared.offerings()

                guard let creditsOffering = offerings["credits"] else {
                    throw PaywallError.noProducts
                }

                guard let package = creditsOffering.availablePackages.first(where: { $0.id == creditsPackage.id }) else {
                    throw PaywallError.productNotFound
                }
                let result = try await Purchases.shared.purchase(package: package)
                if result.userCancelled { throw PaywallError.cancelled }
            },
            restore: {
                try await Purchases.shared.restorePurchases().sunoEntitlements()
            },
            syncPurchases: {
                guard let purchases = try? await Purchases.shared.syncPurchases() else { return [] }
                return try await purchases.sunoEntitlements()
            },
            entitlement: {
                try await Purchases.shared.customerInfo().sunoEntitlements()
            },
            collectDeviceIdentifiers: {
                /// This can be called multiple times without issue, and should be called when ATT permissioning changes
                Purchases.shared.attribution.collectDeviceIdentifiers()
            },
            loadSubscriptionPage: {
                return try await apiClient.getSubscriptionPage()
            }
        )
    }
}

private extension Subscription.Period {
    init(_ value: SubscriptionPeriod.Unit) {
        switch value {
        case .day: self = .day
        case .week: self = .week
        case .month: self = .month
        case .year: self = .year
        }
    }
}

private extension CustomerInfo {
    // swiftlint:disable:next cyclomatic_complexity
    func sunoEntitlements() async throws -> [Entitlement] {
        return try entitlements.active
            .filter(\.value.isActive)
            .sorted { lhs, rhs in
                switch (lhs.value.latestPurchaseDate, rhs.value.latestPurchaseDate) {
                case let (l?, r?):
                    return l > r
                case (nil, nil):
                    return false
                case (nil, _):
                    return false
                case (_, nil):
                    return true
                }
            }
            .map { key, value in
                let productIdentifier = value.productIdentifier
                let period: Subscription.Period
                let plan: Subscription.SemanticPlan
                var isLegacy: Bool = false

                switch key {
                case "pro":
                    plan = .pro
                    switch productIdentifier {
                    case "monthly.pro":
                        period = .month
                    case "monthly.pro_2025_05_01_a",
                        "monthly.pro_2025_05_01_b":
                        period = .month
                        isLegacy = true

                    case "yearly.pro":
                        period = .year
                    case "yearly.pro_2025_05_01_a",
                         "yearly.pro_2025_05_01_b":
                        period = .year
                        isLegacy = true
                    default:
                        throw PaywallError.configurationIssue
                    }

                case "premier":
                    plan = .premier
                    switch productIdentifier {
                    case "monthly.premier":
                        period = .month
                    case "monthly.premier_2025_05_01_a",
                         "monthly.premier_2025_05_01_b":
                        period = .month
                        isLegacy = true

                    case "yearly.premier":
                        period = .year
                    case "yearly.premier_2025_05_01_a",
                         "yearly.premier_2025_05_01_b":
                        period = .year
                        isLegacy = true
                    default:
                        throw PaywallError.configurationIssue
                    }

                case "basic":
                    plan = .basic
                    isLegacy = true
                    switch productIdentifier {
                    case "monthly.basic_2025_05_01_a":
                        period = .month
                    case "yearly.basic_2025_05_01_a":
                        period = .year
                    default:
                        throw PaywallError.configurationIssue
                    }

                default:
                    throw PaywallError.configurationIssue
                }

                return Entitlement(
                    period: period,
                    plan: plan,
                    planIdentifier: productIdentifier,
                    isLegacy: isLegacy,
                    unsubscribed: value.unsubscribeDetectedAt != nil || !value.isActive
                )
            }
    }
}

extension PaywallClient: TestDependencyKey {
    public static let previewValue = Self.noop

    public static let testValue = Self(
        configure: {},
        updateUser: { _ in },
        subscriptions: { _ in
            SubscriptionsConfiguration(subscriptions: [], currencyCode: "USD")
        },
        creditsPackages: { [] },
        purchase: { _, _ in [] },
        purchaseCreditsPackage: { _ in },
        restore: { [] },
        syncPurchases: { [] },
        entitlement: { [] },
        collectDeviceIdentifiers: {},
        loadSubscriptionPage: { nil }
    )
}

public extension PaywallClient {
    static let noop = Self()

    static let failing = Self(
        configure: {},
        updateUser: { _ in },
        subscriptions: { _ in throw PaywallError.configurationIssue },
        creditsPackages: { throw PaywallError.configurationIssue },
        purchase: { _, _ in throw PaywallError.configurationIssue },
        purchaseCreditsPackage: { _ in throw PaywallError.configurationIssue },
        restore: { throw PaywallError.configurationIssue },
        syncPurchases: { throw PaywallError.configurationIssue },
        entitlement: { throw PaywallError.configurationIssue },
        collectDeviceIdentifiers: {},
        loadSubscriptionPage: { throw PaywallError.configurationIssue }
    )
}
