import StoreKit
import SwiftUI

@MainActor
final class StoreManager: ObservableObject {
    enum ProductLoadState {
        case idle
        case loading
        case loaded
        case timedOut
        case notFound
        case failed
    }

    enum PurchaseResult {
        case success
        case cancelled
        case pending
        case failed
    }

    @Published var products: [Product] = []
    @Published var isPremium: Bool = false
    @Published var isLoading: Bool = false
    @Published var isLoadingProducts: Bool = false
    @Published var productLoadState: ProductLoadState = .idle
    @Published var debugLoadedProductIds: [String] = []
    @Published var debugLoadedProducts: [String] = []

    static let monthlyProductId = "com.mealmood.premium.monthly"
    private var productIds: [String] {
        var ids = [Self.monthlyProductId]

        if let bundleId = Bundle.main.bundleIdentifier {
            ids.append("\(bundleId).premium.monthly")
        }

        var seen = Set<String>()
        return ids.filter { seen.insert($0).inserted }
    }

    init() {
        Task {
            await loadProducts()
            await checkPremiumStatus()
        }
    }

    func loadProducts() async {
        isLoadingProducts = true
        productLoadState = .loading
        defer { isLoadingProducts = false }
        do {
            let fetchedProducts = try await loadProductsWithRetries()
            products = fetchedProducts
            debugLoadedProductIds = fetchedProducts.map(\.id)
            debugLoadedProducts = fetchedProducts.map { "\($0.id) [\($0.type)]" }
            productLoadState = fetchedProducts.isEmpty ? .notFound : .loaded
        } catch {
            if error is TimeoutError {
                productLoadState = .timedOut
            } else {
                productLoadState = .failed
            }
            print("Failed to load products: \(error)")
        }
    }

    func purchase(_ product: Product) async -> PurchaseResult {
        isLoading = true
        defer { isLoading = false }

        do {
            let result = try await product.purchase()
            switch result {
            case .success(let verification):
                if case .verified(let transaction) = verification {
                    await transaction.finish()
                    isPremium = true
                    return .success
                }
                return .failed
            case .userCancelled:
                return .cancelled
            case .pending:
                return .pending
            @unknown default:
                return .failed
            }
        } catch {
            print("Purchase failed: \(error)")
            return .failed
        }
    }

    func restorePurchases() async {
        isLoading = true
        defer { isLoading = false }

        do {
            try await AppStore.sync()
            await checkPremiumStatus()
        } catch {
            print("Restore failed: \(error)")
        }
    }

    private func checkPremiumStatus() async {
        isPremium = await Self.hasActiveSubscription(productIds: productIds)
    }

    var monthlyProduct: Product? {
        if let match = products.first(where: { product in
            product.id == Self.monthlyProductId && product.type == .autoRenewable
        }) {
            return match
        }

        if let match = products.first(where: { $0.id == Self.monthlyProductId }) {
            return match
        }

        if let match = products.first(where: { $0.id.contains("monthly") && $0.type == .autoRenewable }) {
            return match
        }

        if let match = products.first(where: { $0.type == .autoRenewable }) {
            return match
        }

        return products.first
    }

    var debugProductIds: [String] { productIds }

    static func hasActiveSubscription(productIds: [String] = [
        StoreManager.monthlyProductId
    ]) async -> Bool {
        for await result in Transaction.currentEntitlements {
            if case .verified(let transaction) = result,
               productIds.contains(transaction.productID),
               transaction.revocationDate == nil {
                if let expirationDate = transaction.expirationDate, expirationDate < Date() {
                    continue
                }
                return true
            }
        }
        return false
    }

    private struct TimeoutError: Error {}

    private func loadProductsWithRetries(maxAttempts: Int = 3) async throws -> [Product] {
        var lastProducts: [Product] = []

        for attempt in 1...maxAttempts {
            let products = try await loadProductsWithTimeout(seconds: 12)
            if !products.isEmpty {
                return products
            }
            lastProducts = products

            if attempt < maxAttempts {
                try await Task.sleep(nanoseconds: 800_000_000)
            }
        }

        return lastProducts
    }

    private func loadProductsWithTimeout(seconds: UInt64) async throws -> [Product] {
        let ids = productIds
        return try await withThrowingTaskGroup(of: [Product].self) { group in
            group.addTask {
                try await self.loadProductsBySingleID(ids: ids)
            }
            group.addTask {
                try await Task.sleep(nanoseconds: seconds * 1_000_000_000)
                throw TimeoutError()
            }

            let firstResult = try await group.next() ?? []
            group.cancelAll()
            return firstResult
        }
    }

    private func loadProductsBySingleID(ids: [String]) async throws -> [Product] {
        var merged: [String: Product] = [:]

        // First, attempt a single batch request with all IDs.
        let batchItems = try await Product.products(for: ids)
        for item in batchItems {
            merged[item.id] = item
        }
        if !merged.isEmpty {
            return Array(merged.values)
        }

        // Fallback to one-by-one requests for better resilience/debugging.
        for id in ids {
            let items = try await Product.products(for: [id])
            for item in items {
                merged[item.id] = item
            }
        }
        return Array(merged.values)
    }
}
