//
//  IAPManager.swift
//  Reflect
//
//  Refactored StoreKit 2 subscription manager with clean state model.
//

import Foundation
import StoreKit
import SwiftUI
import os.log

// MARK: - Subscription State

enum SubscriptionState: Equatable {
    case unknown
    case subscribed(expirationDate: Date?, willAutoRenew: Bool)
    case billingRetry(expirationDate: Date?, willAutoRenew: Bool)
    case gracePeriod(expirationDate: Date?, willAutoRenew: Bool)
    case inTrial(daysRemaining: Int)
    case trialExpired
    case expired
    case revoked
}

// MARK: - IAPManager

@MainActor
class IAPManager: ObservableObject {

    // MARK: - Shared Instance

    /// Shared instance for service-level access (e.g., HealthKit gating)
    static let shared = IAPManager()

    // MARK: - Debug Toggle

    /// Set to `true` to bypass all subscription checks and grant full access (for development only)
    /// Togglable at runtime in DEBUG builds via Settings > Debug > Bypass Subscription
    #if DEBUG
    @Published var bypassSubscription: Bool {
        didSet { UserDefaults.standard.set(bypassSubscription, forKey: "debug_bypassSubscription") }
    }
    #else
    let bypassSubscription = false
    #endif

    // MARK: - Constants

    static let subscriptionGroupID = "21951685"

    static let productIdentifiers: Set<String> = [
        "com.88oakapps.reflect.IAP.subscriptions.monthly",
        "com.88oakapps.reflect.IAP.subscriptions.yearly"
    ]

    private let trialDays = 30

    // MARK: - Published State

    @Published private(set) var state: SubscriptionState = .unknown
    @Published private(set) var availableProducts: [Product] = []
    @Published private(set) var currentProduct: Product? = nil
    @Published private(set) var isLoading = false

    // MARK: - Storage

    /// Reads firstLaunchDate directly from UserDefaults to ensure we always get the latest value
    /// (Using @AppStorage in a class doesn't auto-sync when other components update the same key)
    private var firstLaunchDate: Date {
        get {
            GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue) as? Date ?? Date()
        }
        set {
            GroupUserDefaults.groupDefaults.set(newValue, forKey: UserDefaultsStore.Keys.firstLaunchDate.rawValue)
        }
    }

    // MARK: - Private

    private var updateListenerTask: Task<Void, Error>?

    /// Last time subscription status was checked (for throttling)
    private var lastStatusCheckTime: Date?

    /// Minimum interval between status checks (5 minutes)
    private let statusCheckInterval: TimeInterval = 300

    // MARK: - Computed Properties

    var isSubscribed: Bool {
        switch state {
        case .subscribed, .billingRetry, .gracePeriod:
            return true
        default:
            return false
        }
    }

    var hasFullAccess: Bool {
        if bypassSubscription { return true }
        switch state {
        case .subscribed, .billingRetry, .gracePeriod, .inTrial:
            return true
        case .unknown, .trialExpired, .expired, .revoked:
            return false
        }
    }

    var shouldShowPaywall: Bool {
        if bypassSubscription { return false }
        switch state {
        case .trialExpired, .expired, .revoked:
            return true
        case .unknown, .subscribed, .billingRetry, .gracePeriod, .inTrial:
            return false
        }
    }

    var shouldShowTrialWarning: Bool {
        if bypassSubscription { return false }
        if case .inTrial = state { return true }
        return false
    }

    var daysLeftInTrial: Int {
        if case .inTrial(let days) = state { return days }
        return 0
    }

    var trialExpirationDate: Date? {
        Calendar.current.date(byAdding: .day, value: trialDays, to: firstLaunchDate)
    }

    /// Products sorted by price (lowest first)
    var sortedProducts: [Product] {
        availableProducts.sorted { $0.price < $1.price }
    }

    // MARK: - Initialization

    init() {
        #if DEBUG
        self.bypassSubscription = UserDefaults.standard.bool(forKey: "debug_bypassSubscription")
        #endif
        restoreCachedSubscriptionState()
        updateListenerTask = listenForTransactions()

        Task {
            await checkSubscriptionStatus()
        }
    }

    deinit {
        updateListenerTask?.cancel()
    }

    // MARK: - Public Methods

    /// Check subscription status - call on app launch and when becoming active
    /// Throttled to avoid excessive StoreKit calls on rapid foreground transitions
    /// Pass `force: true` to bypass throttle (e.g. after a transaction update)
    func checkSubscriptionStatus(force: Bool = false) async {
        AppLogger.iap.debug("checkSubscriptionStatus: called (force=\(force), currentState=\(String(describing: self.state)))")

        // Throttle: skip if we checked recently (unless state is unknown or forced)
        if !force,
           state != .unknown,
           let lastCheck = lastStatusCheckTime,
           Date().timeIntervalSince(lastCheck) < statusCheckInterval {
            AppLogger.iap.debug("checkSubscriptionStatus: THROTTLED (last check \(Date().timeIntervalSince(lastCheck))s ago)")
            return
        }

        // Only update isLoading if value actually changes to avoid unnecessary view updates
        if !isLoading { isLoading = true }
        defer {
            if isLoading { isLoading = false }
            lastStatusCheckTime = Date()
            AppLogger.iap.debug("checkSubscriptionStatus: DONE — final state=\(String(describing: self.state))")
        }

        // Fetch available products
        await loadProducts()

        // Check for active subscription
        let hasActiveSubscription = await checkForActiveSubscription()
        AppLogger.iap.debug("checkSubscriptionStatus: hasActiveSubscription=\(hasActiveSubscription), state after check=\(String(describing: self.state))")

        if hasActiveSubscription {
            // State already set in checkForActiveSubscription — cache it
            switch state {
            case .subscribed(let expiration, _),
                 .billingRetry(let expiration, _),
                 .gracePeriod(let expiration, _):
                cacheSubscriptionExpiration(expiration)
            default:
                break
            }
            syncSubscriptionStatusToUserDefaults()
            trackSubscriptionAnalytics(source: "status_check_active")
            return
        }

        // Live check found no active subscription.
        // Only trust cached expiration if we're offline (products failed to load).
        // If products loaded successfully, StoreKit had server access and the live result is authoritative.
        if availableProducts.isEmpty {
            let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
            AppLogger.iap.debug("checkSubscriptionStatus: offline (no products) — cachedExpiration=\(cachedExpiration?.description ?? "nil")")

            if let expiration = cachedExpiration, expiration > Date() {
                AppLogger.iap.debug("checkSubscriptionStatus: using cached expiration (offline, still valid)")
                state = .subscribed(expirationDate: expiration, willAutoRenew: false)
                syncSubscriptionStatusToUserDefaults()
                trackSubscriptionAnalytics(source: "status_check_cached")
                return
            }
        }

        // Subscription genuinely gone — clear cache and fall back to trial
        AppLogger.iap.debug("checkSubscriptionStatus: falling back to trial — firstLaunchDate=\(self.firstLaunchDate), trialDays=\(self.trialDays)")
        cacheSubscriptionExpiration(nil)
        updateTrialState()
        trackSubscriptionAnalytics(source: "status_check_fallback")
    }

    /// Sync subscription status to UserDefaults for widget access
    private func syncSubscriptionStatusToUserDefaults() {
        let accessValue = bypassSubscription ? true : hasFullAccess
        GroupUserDefaults.groupDefaults.set(accessValue, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
    }

    /// Cache subscription state so premium access survives offline/failed checks
    private func cacheSubscriptionExpiration(_ date: Date?) {
        GroupUserDefaults.groupDefaults.set(date, forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue)
    }

    /// Restore cached subscription state on launch (before async check completes)
    private func restoreCachedSubscriptionState() {
        let hasActive = GroupUserDefaults.groupDefaults.bool(forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
        let cachedExpiration = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue) as? Date
        AppLogger.iap.debug("restoreCachedSubscriptionState: hasActive=\(hasActive), cachedExpiration=\(cachedExpiration?.description ?? "nil")")

        guard hasActive else {
            AppLogger.iap.debug("restoreCachedSubscriptionState: no cached active state, skipping")
            return
        }

        // If we have a cached expiration and it's still in the future, restore subscribed state
        if let expiration = cachedExpiration, expiration > Date() {
            AppLogger.iap.debug("restoreCachedSubscriptionState: restoring .subscribed (cached expiration in future)")
            state = .subscribed(expirationDate: expiration, willAutoRenew: false)
        } else if cachedExpiration == nil && hasActive {
            // Had access but no expiration cached (e.g., upgraded from older version) — trust it
            AppLogger.iap.debug("restoreCachedSubscriptionState: restoring .subscribed (no expiration, trusting hasActive)")
            state = .subscribed(expirationDate: nil, willAutoRenew: false)
        } else {
            AppLogger.iap.debug("restoreCachedSubscriptionState: cached expiration in past, not restoring")
        }
    }

    /// Restore purchases
    func restore(source: String = "settings") async {
        do {
            try await AppStore.sync()
            await checkSubscriptionStatus(force: true)
            AnalyticsManager.shared.trackPurchaseRestored(source: source)
            trackSubscriptionAnalytics(source: "restore")
        } catch {
            AnalyticsManager.shared.trackPurchaseFailed(productId: nil, source: source, error: error.localizedDescription)
            AppLogger.iap.error("Failed to restore purchases: \(error.localizedDescription)")
        }
    }

    // MARK: - Private Methods

    private func loadProducts() async {
        AppLogger.iap.info("loadProducts() called — requesting: \(Self.productIdentifiers.sorted().joined(separator: ", "))")
        do {
            let products = try await Product.products(for: Self.productIdentifiers)
            AppLogger.iap.info("loadProducts() returned \(products.count) products total")
            for product in products {
                AppLogger.iap.info("  Product: \(product.id) — type: \(product.type.rawValue), displayName: \(product.displayName), price: \(product.displayPrice)")
            }
            availableProducts = products.filter { $0.type == .autoRenewable }
            AppLogger.iap.info("loadProducts() filtered to \(self.availableProducts.count) autoRenewable products")
        } catch {
            AppLogger.iap.error("loadProducts() FAILED: \(error.localizedDescription) — full error: \(String(describing: error))")
        }
    }

    private func checkForActiveSubscription() async -> Bool {
        var nonActiveState: SubscriptionState?

        for await result in Transaction.currentEntitlements {
            guard case .verified(let transaction) = result else { continue }

            // Skip revoked transactions
            if transaction.revocationDate != nil { continue }

            // Check if this is one of our subscription products
            guard Self.productIdentifiers.contains(transaction.productID) else { continue }

            // Get the product for this transaction
            currentProduct = availableProducts.first { $0.id == transaction.productID }

            // Get renewal info
            if let product = currentProduct,
               let subscription = product.subscription,
               let statuses = try? await subscription.status {
                var hadVerifiedStatus = false

                for status in statuses {
                    guard case .verified(let renewalInfo) = status.renewalInfo else { continue }
                    hadVerifiedStatus = true

                    switch status.state {
                    case .subscribed:
                        state = .subscribed(
                            expirationDate: transaction.expirationDate,
                            willAutoRenew: renewalInfo.willAutoRenew
                        )
                        return true
                    case .inBillingRetryPeriod:
                        state = .billingRetry(
                            expirationDate: transaction.expirationDate,
                            willAutoRenew: renewalInfo.willAutoRenew
                        )
                        return true
                    case .inGracePeriod:
                        state = .gracePeriod(
                            expirationDate: transaction.expirationDate,
                            willAutoRenew: renewalInfo.willAutoRenew
                        )
                        return true
                    case .expired:
                        nonActiveState = .expired
                        continue
                    case .revoked:
                        nonActiveState = .revoked
                        continue
                    default:
                        continue
                    }
                }

                // We had detailed status and none were active, so do not fallback to subscribed.
                if hadVerifiedStatus {
                    continue
                }
            }

            // Fallback if we couldn't get detailed status
            state = .subscribed(expirationDate: transaction.expirationDate, willAutoRenew: false)
            return true
        }

        // No active subscription found
        if let nonActiveState {
            state = nonActiveState
        } else {
            currentProduct = nil
        }
        return false
    }

    #if DEBUG
    /// Reset subscription state for UI testing. Called after group defaults are cleared
    /// so that stale cached state from previous test runs is discarded.
    func resetForTesting() {
        state = .unknown
        lastStatusCheckTime = nil
        currentProduct = nil
        availableProducts = []

        // Explicitly clear cached subscription state to prevent async
        // checkSubscriptionStatus from restoring stale .subscribed state.
        GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.cachedSubscriptionExpiration.rawValue)
        GroupUserDefaults.groupDefaults.set(false, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue)
        GroupUserDefaults.groupDefaults.synchronize()

        updateTrialState()
    }
    #endif

    private func updateTrialState() {
        let daysSinceInstall = Calendar.current.dateComponents([.day], from: firstLaunchDate, to: Date()).day ?? 0
        let daysRemaining = trialDays - daysSinceInstall
        AppLogger.iap.debug("updateTrialState: firstLaunchDate=\(self.firstLaunchDate), daysSinceInstall=\(daysSinceInstall), daysRemaining=\(daysRemaining)")

        if daysRemaining > 0 {
            AppLogger.iap.debug("updateTrialState: setting .inTrial(daysRemaining: \(daysRemaining))")
            state = .inTrial(daysRemaining: daysRemaining)
        } else {
            AppLogger.iap.debug("updateTrialState: setting .trialExpired")
            state = .trialExpired
        }

        syncSubscriptionStatusToUserDefaults()
    }

    // MARK: - Analytics

    func trackSubscriptionAnalytics(source: String) {
        let status: String
        let isSubscribed: Bool
        let hasFullAccess: Bool
        let productId = currentProduct?.id
        let type = subscriptionType(for: productId)
        let willAutoRenew: Bool?
        let isInGracePeriod: Bool?
        let trialDaysRemaining: Int?
        let expirationDate: Date?

        switch state {
        case .unknown:
            status = "unknown"
            isSubscribed = false
            hasFullAccess = bypassSubscription
            willAutoRenew = nil
            isInGracePeriod = nil
            trialDaysRemaining = nil
            expirationDate = nil
        case .subscribed(let expiration, let autoRenew):
            status = "subscribed"
            isSubscribed = true
            hasFullAccess = true
            willAutoRenew = autoRenew
            isInGracePeriod = false
            trialDaysRemaining = nil
            expirationDate = expiration
        case .billingRetry(let expiration, let autoRenew):
            status = "billing_retry"
            isSubscribed = true
            hasFullAccess = true
            willAutoRenew = autoRenew
            isInGracePeriod = true
            trialDaysRemaining = nil
            expirationDate = expiration
        case .gracePeriod(let expiration, let autoRenew):
            status = "grace_period"
            isSubscribed = true
            hasFullAccess = true
            willAutoRenew = autoRenew
            isInGracePeriod = true
            trialDaysRemaining = nil
            expirationDate = expiration
        case .inTrial(let daysRemaining):
            status = "trial"
            isSubscribed = false
            hasFullAccess = true
            willAutoRenew = nil
            isInGracePeriod = nil
            trialDaysRemaining = daysRemaining
            expirationDate = nil
        case .trialExpired:
            status = "trial_expired"
            isSubscribed = false
            hasFullAccess = false
            willAutoRenew = nil
            isInGracePeriod = nil
            trialDaysRemaining = nil
            expirationDate = nil
        case .expired:
            status = "expired"
            isSubscribed = false
            hasFullAccess = false
            willAutoRenew = nil
            isInGracePeriod = nil
            trialDaysRemaining = nil
            expirationDate = nil
        case .revoked:
            status = "revoked"
            isSubscribed = false
            hasFullAccess = false
            willAutoRenew = nil
            isInGracePeriod = nil
            trialDaysRemaining = nil
            expirationDate = nil
        }

        AnalyticsManager.shared.trackSubscriptionStatusObserved(
            status: status,
            type: type,
            source: source,
            isSubscribed: isSubscribed,
            hasFullAccess: hasFullAccess,
            productId: productId,
            willAutoRenew: willAutoRenew,
            isInGracePeriod: isInGracePeriod,
            trialDaysRemaining: trialDaysRemaining,
            expirationDate: expirationDate
        )
    }

    private func subscriptionType(for productID: String?) -> String {
        guard let productID else { return "none" }
        let id = productID.lowercased()
        if id.contains("annual") || id.contains("year") {
            return "yearly"
        }
        if id.contains("month") {
            return "monthly"
        }
        return "unknown"
    }

    private func listenForTransactions() -> Task<Void, Error> {
        Task.detached { [weak self] in
            for await result in Transaction.updates {
                guard case .verified(let transaction) = result else { continue }

                await transaction.finish()
                await self?.checkSubscriptionStatus(force: true)
            }
        }
    }
}
