import ExpoModulesCore
import Foundation
import OpenIAP
#if canImport(UIKit)
import UIKit
#endif


@available(iOS 15.0, tvOS 15.0, *)
@MainActor
public final class ExpoIapModule: Module {
    private var isInitialized = false
    private var listenerGeneration: UInt64?

    nonisolated public func definition() -> ModuleDefinition {
        Name("ExpoIap")

        Constant("ERROR_CODES") {
            OpenIapSerialization.errorCodes()
        }

        Constant("HAS_EXPLICIT_STORE_PROVIDER") {
            Bundle.main.object(forInfoDictionaryKey: OpenIapProvider.metadataKey) != nil
        }

        Events(
            IapEvent.purchaseUpdated.rawValue,
            IapEvent.purchaseError.rawValue,
            IapEvent.promotedProductIos.rawValue,
            IapEvent.subscriptionBillingIssue.rawValue
        )

        OnCreate { [weak self] in
            Task { @MainActor [weak self] in
                guard let self else { return }
                self.listenerGeneration = ExpoIapHelper.setupStore(module: self)
            }
        }

        OnDestroy { [weak self] in
            guard let self else { return }
            Task { @MainActor [self] in
                guard let listenerGeneration = self.listenerGeneration else { return }
                self.listenerGeneration = nil
                await ExpoIapHelper.cleanupStore(listenerGeneration: listenerGeneration)
            }
        }

        AsyncFunction("initConnection") { (config: [String: Any]?) async throws -> Bool in
            try await IapException.bridging("initConnection", fallback: .initConnection) {
                // iOS ignores the config; it has no alternative billing parameter.
                await ExpoIapHelper.waitForStoreCleanup()
                let isConnected = try await OpenIapModule.shared.initConnection()
                await MainActor.run { self.isInitialized = isConnected }
                return isConnected
            }
        }

        AsyncFunction("endConnection") { () async throws -> Bool in
            try await IapException.bridging("endConnection") {
                let succeeded = try await OpenIapModule.shared.endConnection()
                await MainActor.run { self.isInitialized = false }
                return succeeded
            }
        }

        AsyncFunction("setPurchaseUpdatedListenerOptions") { (options: [String: Any]?) async throws -> Void in
            let listenerOptions = PurchaseUpdatedListenerOptions(
                dedupeTransactionIOS:
                    options?["dedupeTransactionIOS"] as? Bool
            )
            ExpoIapHelper.setPurchaseUpdatedListenerOptions(listenerOptions)
        }

        AsyncFunction("fetchProducts") { (params: [String: Any]) async throws -> [[String: Any]] in
            try await IapException.bridging("fetchProducts", fallback: .queryProduct) {
                ExpoIapLog.payload("fetchProducts", payload: params)
                let request = try ExpoIapHelper.decodeProductRequest(from: params)
                let result = try await OpenIapModule.shared.fetchProducts(request)
                let products = ExpoIapHelper.sanitizeArray(OpenIapSerialization.products(result))
                ExpoIapLog.result("fetchProducts", value: products)
                return products
            }
        }

        AsyncFunction("requestPurchase") { (payload: [String: Any]) async throws -> Any? in
            try await IapException.bridging("requestPurchase", fallback: .purchaseError) {
                ExpoIapLog.payload("requestPurchase", payload: payload)
                let props = try ExpoIapHelper.decodeRequestPurchaseProps(from: payload)
                guard let result = try await OpenIapModule.shared.requestPurchase(props) else {
                    ExpoIapLog.result("requestPurchase", value: nil)
                    return nil
                }

                switch result {
                case .purchase(let maybePurchase):
                    guard let purchase = maybePurchase else { return nil }
                    let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.purchase(purchase))
                    ExpoIapLog.result("requestPurchase", value: sanitized)
                    return sanitized
                case .purchases(let maybePurchases):
                    guard let purchases = maybePurchases else { return nil }
                    let sanitized = ExpoIapHelper.sanitizeArray(OpenIapSerialization.purchases(purchases))
                    ExpoIapLog.result("requestPurchase", value: sanitized)
                    return sanitized
                }
            }
        }

        AsyncFunction("finishTransaction") {
            (purchasePayload: [String: Any], isConsumable: Bool?) async throws -> Bool in
            try await IapException.bridging("finishTransaction") {
                ExpoIapLog.payload(
                    "finishTransaction",
                    payload: [
                        "purchase": purchasePayload,
                        "isConsumable": isConsumable as Any,
                    ]
                )
                let purchaseInput = try OpenIapSerialization.purchaseInput(from: purchasePayload)
                try await OpenIapModule.shared.finishTransaction(
                    purchase: purchaseInput,
                    isConsumable: isConsumable
                )
                ExpoIapLog.result("finishTransaction", value: true)
                return true
            }
        }

        // Internal: backs the JS first-purchase notice; not app API.
        AsyncFunction("claimFirstPurchaseNotice") { () -> Bool in
            OpenIapFirstPurchaseNotice.claim()
        }

        AsyncFunction("getAvailableItems") {
            (alsoPublish: Bool, onlyIncludeActive: Bool) async throws -> [[String: Any]] in
            try await IapException.bridging("getAvailableItems") {
                ExpoIapLog.payload(
                    "getAvailableItems",
                    payload: [
                        "alsoPublishToEventListenerIOS": alsoPublish,
                        "onlyIncludeActiveItemsIOS": onlyIncludeActive,
                    ]
                )
                let optionsDictionary: [String: Any] = [
                    "alsoPublishToEventListenerIOS": alsoPublish,
                    "onlyIncludeActiveItemsIOS": onlyIncludeActive
                ]
                let options = try OpenIapSerialization.purchaseOptions(from: optionsDictionary)
                let purchases = try await OpenIapModule.shared.getAvailablePurchases(options)
                let sanitized = ExpoIapHelper.sanitizeArray(try ExpoIapHelper.purchasesRequired(purchases))
                ExpoIapLog.result("getAvailableItems", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("getPendingTransactionsIOS") { () async throws -> [[String: Any]] in
            try await IapException.bridging("getPendingTransactionsIOS") {
                ExpoIapLog.payload("getPendingTransactionsIOS", payload: nil)
                let pending = try await OpenIapModule.shared.getPendingTransactionsIOS()
                let sanitized = try pending.map {
                    ExpoIapHelper.sanitizeDictionary(try ExpoIapHelper.encodeRequired($0))
                }
                ExpoIapLog.result("getPendingTransactionsIOS", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("getAllTransactionsIOS") { () async throws -> [[String: Any]] in
            try await IapException.bridging("getAllTransactionsIOS") {
                ExpoIapLog.payload("getAllTransactionsIOS", payload: nil)
                let all = try await OpenIapModule.shared.getAllTransactionsIOS()
                let sanitized = try all.map {
                    ExpoIapHelper.sanitizeDictionary(try ExpoIapHelper.encodeRequired($0))
                }
                ExpoIapLog.result("getAllTransactionsIOS", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("clearTransactionIOS") { () async throws -> Bool in
            try await IapException.bridging("clearTransactionIOS") {
                ExpoIapLog.payload("clearTransactionIOS", payload: nil)
                let success = try await OpenIapModule.shared.clearTransactionIOS()
                ExpoIapLog.result("clearTransactionIOS", value: success)
                return success
            }
        }

        AsyncFunction("getReceiptDataIOS") { () async throws -> String in
            try await IapException.bridging("getReceiptDataIOS") {
                ExpoIapLog.payload("getReceiptDataIOS", payload: nil)
                let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
                ExpoIapLog.result("getReceiptDataIOS", value: "<receipt>")
                return receipt
            }
        }

        AsyncFunction("requestReceiptRefreshIOS") { () async throws -> String in
            try await IapException.bridging("requestReceiptRefreshIOS", fallback: .syncError) {
                ExpoIapLog.payload("requestReceiptRefreshIOS", payload: nil)
                _ = try await OpenIapModule.shared.syncIOS()
                let receipt = try await OpenIapModule.shared.getReceiptDataIOS() ?? ""
                ExpoIapLog.result("requestReceiptRefreshIOS", value: "<receipt>")
                return receipt
            }
        }

        AsyncFunction("verifyAppOwnership") { (params: [String: Any]) async throws -> [String: Any] in
            try await IapException.bridging("verifyAppOwnership") {
                let options = try OpenIapSerialization.decode(object: params, as: VerifyAppOwnershipProps.self)
                let result = try await OpenIapModule.shared.verifyAppOwnership(options)
                return ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
            }
        }

        AsyncFunction("verifyPurchase") { (params: [String: Any]) async throws -> [String: Any] in
            try await IapException.bridging("verifyPurchase", fallback: .purchaseVerificationFailed) {
                ExpoIapLog.payload(
                    "verifyPurchase",
                    payload: [
                        "hasApple": params["apple"] != nil,
                        "hasGoogle": params["google"] != nil,
                        "hasHorizon": params["horizon"] != nil,
                    ]
                )
                let props = try OpenIapSerialization.verifyPurchaseProps(from: params)
                let result = try await OpenIapModule.shared.verifyPurchase(props)
                // The VerifyPurchaseResult union encodes flat, with `__typename`.
                let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
                ExpoIapLog.result("verifyPurchase", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("verifyPurchaseWithProvider") { (params: [String: Any]) async throws -> [String: Any] in
            try await IapException.bridging("verifyPurchaseWithProvider", fallback: .purchaseVerificationFailed) {
                ExpoIapLog.payload(
                    "verifyPurchaseWithProvider",
                    payload: [
                        "provider": params["provider"] ?? "unknown",
                        "hasIapkit": params["iapkit"] != nil
                    ]
                )
                let jsonData = try JSONSerialization.data(withJSONObject: params)
                let props = try JSONDecoder().decode(VerifyPurchaseWithProviderProps.self, from: jsonData)
                let result = try await OpenIapModule.shared.verifyPurchaseWithProvider(props)
                let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
                ExpoIapLog.result("verifyPurchaseWithProvider", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("openRedeemOfferCode") { () async throws -> [String: Any]? in
            try await IapException.bridging("openRedeemOfferCode", fallback: .purchaseError) {
                ExpoIapLog.payload("openRedeemOfferCode", payload: nil)
                guard let purchase = try await OpenIapModule.shared.openRedeemOfferCode() else {
                    ExpoIapLog.result("openRedeemOfferCode", value: nil)
                    return nil
                }
                let payload = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(purchase))
                ExpoIapLog.result("openRedeemOfferCode", value: payload)
                return payload
            }
        }

        AsyncFunction("showManageSubscriptionsIOS") { () async throws -> [[String: Any]] in
            try await IapException.bridging("showManageSubscriptionsIOS") {
                ExpoIapLog.payload("showManageSubscriptionsIOS", payload: nil)
                let purchases = try await OpenIapModule.shared.showManageSubscriptionsIOS()
                let sanitized = try purchases.map {
                    ExpoIapHelper.sanitizeDictionary(try ExpoIapHelper.encodeRequired($0))
                }
                ExpoIapLog.result("showManageSubscriptionsIOS", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("deepLinkToSubscriptionsIOS") { () async throws -> Bool in
            try await IapException.bridging("deepLinkToSubscriptionsIOS") {
                ExpoIapLog.payload("deepLinkToSubscriptionsIOS", payload: nil)
                try await OpenIapModule.shared.deepLinkToSubscriptions(nil)
                ExpoIapLog.result("deepLinkToSubscriptionsIOS", value: true)
                return true
            }
        }

        AsyncFunction("beginRefundRequestIOS") { (sku: String) async throws -> String? in
            try await IapException.bridging("beginRefundRequestIOS") {
                ExpoIapLog.payload("beginRefundRequestIOS", payload: ["sku": sku])
                let result = try await OpenIapModule.shared.beginRefundRequestIOS(sku: sku)
                ExpoIapLog.result("beginRefundRequestIOS", value: result)
                return result
            }
        }

        AsyncFunction("getPromotedProductIOS") { () async throws -> [String: Any]? in
            try await IapException.bridging("getPromotedProductIOS") {
                ExpoIapLog.payload("getPromotedProductIOS", payload: nil)
                if let product = try await OpenIapModule.shared.getPromotedProductIOS() {
                    let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(product))
                    ExpoIapLog.result("getPromotedProductIOS", value: sanitized)
                    return sanitized
                }
                ExpoIapLog.result("getPromotedProductIOS", value: nil)
                return nil
            }
        }

        AsyncFunction("getStorefront") { () async throws -> String in
            try await IapException.bridging("getStorefront") {
                ExpoIapLog.payload("getStorefront", payload: nil)
                let storefront = try await OpenIapModule.shared.getStorefront()
                ExpoIapLog.result("getStorefront", value: storefront)
                return storefront
            }
        }

        AsyncFunction("restorePurchases") { () async throws -> Bool in
            try await IapException.bridging("restorePurchases", fallback: .syncError) {
                try await OpenIapModule.shared.restorePurchases()
                return true
            }
        }

        AsyncFunction("syncIOS") { () async throws -> Bool in
            try await IapException.bridging("syncIOS", fallback: .syncError) {
                ExpoIapLog.payload("syncIOS", payload: nil)
                let success = try await OpenIapModule.shared.syncIOS()
                ExpoIapLog.result("syncIOS", value: success)
                return success
            }
        }

        AsyncFunction("isTransactionVerifiedIOS") { (sku: String) async throws -> Bool in
            try await IapException.bridging("isTransactionVerifiedIOS") {
                ExpoIapLog.payload("isTransactionVerifiedIOS", payload: ["sku": sku])
                let verified = try await OpenIapModule.shared.isTransactionVerifiedIOS(sku: sku)
                ExpoIapLog.result("isTransactionVerifiedIOS", value: verified)
                return verified
            }
        }

        AsyncFunction("getTransactionJwsIOS") { (sku: String) async throws -> String? in
            try await IapException.bridging("getTransactionJwsIOS") {
                ExpoIapLog.payload("getTransactionJwsIOS", payload: ["sku": sku])
                let jws = try await OpenIapModule.shared.getTransactionJwsIOS(sku: sku)
                ExpoIapLog.result("getTransactionJwsIOS", value: jws == nil ? nil : "<jws>")
                return jws
            }
        }

        AsyncFunction("isEligibleForIntroOfferIOS") { (groupID: String) async throws -> Bool in
            try await IapException.bridging("isEligibleForIntroOfferIOS") {
                ExpoIapLog.payload("isEligibleForIntroOfferIOS", payload: ["groupID": groupID])
                let eligible = try await OpenIapModule.shared.isEligibleForIntroOfferIOS(groupID: groupID)
                ExpoIapLog.result("isEligibleForIntroOfferIOS", value: eligible)
                return eligible
            }
        }

        AsyncFunction("subscriptionStatusIOS") { (sku: String) async throws -> [[String: Any]]? in
            try await IapException.bridging("subscriptionStatusIOS") {
                ExpoIapLog.payload("subscriptionStatusIOS", payload: ["sku": sku])
                let statuses = try await OpenIapModule.shared.subscriptionStatusIOS(sku: sku)
                let sanitized = statuses.map { ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode($0)) }
                ExpoIapLog.result("subscriptionStatusIOS", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("currentEntitlementIOS") { (sku: String) async throws -> [String: Any]? in
            try await IapException.bridging("currentEntitlementIOS", fallback: .skuNotFound, productId: sku) {
                ExpoIapLog.payload("currentEntitlementIOS", payload: ["sku": sku])
                if let entitlement = try await OpenIapModule.shared.currentEntitlementIOS(sku: sku) {
                    let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(entitlement))
                    ExpoIapLog.result("currentEntitlementIOS", value: sanitized)
                    return sanitized
                }
                ExpoIapLog.result("currentEntitlementIOS", value: nil)
                return nil
            }
        }

        AsyncFunction("latestTransactionIOS") { (sku: String) async throws -> [String: Any]? in
            try await IapException.bridging("latestTransactionIOS", fallback: .skuNotFound, productId: sku) {
                ExpoIapLog.payload("latestTransactionIOS", payload: ["sku": sku])
                if let transaction = try await OpenIapModule.shared.latestTransactionIOS(sku: sku) {
                    let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(transaction))
                    ExpoIapLog.result("latestTransactionIOS", value: sanitized)
                    return sanitized
                }
                ExpoIapLog.result("latestTransactionIOS", value: nil)
                return nil
            }
        }

        AsyncFunction("getActiveSubscriptions") { (subscriptionIds: [String]?) async throws -> [[String: Any]] in
            try await IapException.bridging("getActiveSubscriptions") {
                ExpoIapLog.payload("getActiveSubscriptions", payload: subscriptionIds.map { ["subscriptionIds": $0] } ?? [:])
                let subscriptions = try await OpenIapModule.shared.getActiveSubscriptions(subscriptionIds)
                let sanitized = try subscriptions.map {
                    ExpoIapHelper.sanitizeDictionary(try ExpoIapHelper.encodeRequired($0))
                }
                ExpoIapLog.result("getActiveSubscriptions", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("hasActiveSubscriptions") { (subscriptionIds: [String]?) async throws -> Bool in
            try await IapException.bridging("hasActiveSubscriptions") {
                ExpoIapLog.payload("hasActiveSubscriptions", payload: subscriptionIds.map { ["subscriptionIds": $0] } ?? [:])
                let hasActive = try await OpenIapModule.shared.hasActiveSubscriptions(subscriptionIds)
                ExpoIapLog.result("hasActiveSubscriptions", value: hasActive)
                return hasActive
            }
        }

        // MARK: - External Purchase

        AsyncFunction("canPresentExternalPurchaseNoticeIOS") { () async throws -> Bool in
            try await IapException.bridging("canPresentExternalPurchaseNoticeIOS") {
                ExpoIapLog.payload("canPresentExternalPurchaseNoticeIOS", payload: nil)
                let canPresent = try await OpenIapModule.shared.canPresentExternalPurchaseNoticeIOS()
                ExpoIapLog.result("canPresentExternalPurchaseNoticeIOS", value: canPresent)
                return canPresent
            }
        }

        AsyncFunction("presentExternalPurchaseNoticeSheetIOS") { () async throws -> [String: Any] in
            try await IapException.bridging("presentExternalPurchaseNoticeSheetIOS") {
                ExpoIapLog.payload("presentExternalPurchaseNoticeSheetIOS", payload: nil)
                let result = try await OpenIapModule.shared.presentExternalPurchaseNoticeSheetIOS()
                let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
                ExpoIapLog.result("presentExternalPurchaseNoticeSheetIOS", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("presentExternalPurchaseLinkIOS") { (url: String) async throws -> [String: Any] in
            try await IapException.bridging("presentExternalPurchaseLinkIOS") {
                ExpoIapLog.payload("presentExternalPurchaseLinkIOS", payload: ["url": url])
                let result = try await OpenIapModule.shared.presentExternalPurchaseLinkIOS(url)
                let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
                ExpoIapLog.result("presentExternalPurchaseLinkIOS", value: sanitized)
                return sanitized
            }
        }

        // MARK: - App Transaction (iOS 16.0+)

        AsyncFunction("getAppTransactionIOS") { () async throws -> [String: Any]? in
            try await IapException.bridging("getAppTransactionIOS") {
                ExpoIapLog.payload("getAppTransactionIOS", payload: nil)
                if #available(iOS 16.0, tvOS 16.0, *) {
                    if let transaction = try await OpenIapModule.shared.getAppTransactionIOS() {
                        let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(transaction))
                        ExpoIapLog.result("getAppTransactionIOS", value: sanitized)
                        return sanitized
                    }
                }
                ExpoIapLog.result("getAppTransactionIOS", value: nil)
                return nil
            }
        }

        // MARK: - ExternalPurchaseCustomLink (iOS 18.1+)

        AsyncFunction("isEligibleForExternalPurchaseCustomLinkIOS") { () async throws -> Bool in
            try await IapException.bridging("isEligibleForExternalPurchaseCustomLinkIOS") {
                ExpoIapLog.payload("isEligibleForExternalPurchaseCustomLinkIOS", payload: nil)
                let isEligible = try await OpenIapModule.shared.isEligibleForExternalPurchaseCustomLinkIOS()
                ExpoIapLog.result("isEligibleForExternalPurchaseCustomLinkIOS", value: isEligible)
                return isEligible
            }
        }

        AsyncFunction("getExternalPurchaseCustomLinkTokenIOS") { (tokenType: String) async throws -> [String: Any] in
            try await IapException.bridging("getExternalPurchaseCustomLinkTokenIOS") {
                ExpoIapLog.payload("getExternalPurchaseCustomLinkTokenIOS", payload: ["tokenType": tokenType])
                guard let type = ExternalPurchaseCustomLinkTokenTypeIOS(rawValue: tokenType) else {
                    throw PurchaseError.make(
                        code: .developerError,
                        message: "Invalid token type: \(tokenType). Must be 'acquisition' or 'services'"
                    )
                }
                let result = try await OpenIapModule.shared.getExternalPurchaseCustomLinkTokenIOS(type)
                let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
                ExpoIapLog.result("getExternalPurchaseCustomLinkTokenIOS", value: sanitized)
                return sanitized
            }
        }

        AsyncFunction("showExternalPurchaseCustomLinkNoticeIOS") { (noticeType: String) async throws -> [String: Any] in
            try await IapException.bridging("showExternalPurchaseCustomLinkNoticeIOS") {
                ExpoIapLog.payload("showExternalPurchaseCustomLinkNoticeIOS", payload: ["noticeType": noticeType])
                guard let type = ExternalPurchaseCustomLinkNoticeTypeIOS(rawValue: noticeType) else {
                    throw PurchaseError.make(
                        code: .developerError,
                        message: "Invalid notice type: \(noticeType). Must be 'browser'"
                    )
                }
                let result = try await OpenIapModule.shared.showExternalPurchaseCustomLinkNoticeIOS(type)
                let sanitized = ExpoIapHelper.sanitizeDictionary(OpenIapSerialization.encode(result))
                ExpoIapLog.result("showExternalPurchaseCustomLinkNoticeIOS", value: sanitized)
                return sanitized
            }
        }
    }
}
