import Foundation
import StoreKit

@available(iOS 15.0, *)
final class StoreKitObservedModel: ObservableObject {
  private var storeKitTaskHandle: Task<Void, Error>?
  private var storeKitTaskHandleUpdates: Task<Void, Error>?

  @Published var availableProducts: [Product] = []
  @Published var purchasedProducts: Set<Transaction> = []

  init() {
    storeKitTaskHandle = listenForStoreKitEntitlements()
    storeKitTaskHandleUpdates = listenForStoreKitUpdates()
  }

  @MainActor
  private func handleStoreKitUpdates(result: VerificationResult<Transaction>) async {
    switch result {
    case .verified(let transaction):
      if !purchasedProducts.contains(transaction) {
        purchasedProducts.insert(transaction)
      }

      await transaction.finish()

    case .unverified:
      print("Transaction unverified")
    }
  }

  private func listenForStoreKitEntitlements() -> Task<Void, Error> {
    Task.detached {
      for await result in Transaction.currentEntitlements {
        await self.handleStoreKitUpdates(result: result)
      }
    }
  }

  private func listenForStoreKitUpdates() -> Task<Void, Error> {
    Task.detached {
      for await result in Transaction.updates {
        await self.handleStoreKitUpdates(result: result)
      }
    }
  }

  @MainActor
  func products(ids: [String]) async throws -> [Product] {
    do {
      let products = try await Product.products(
        for: Set(ids)
      )

      if products.isEmpty {
        throw PurchaseKitError(
          code: ErrorCode.EMPTY,
          message: "No products found for these ids"
        )
      }

      products.forEach { product in
        if !availableProducts.contains(product) {
          availableProducts.append(product)
        }
      }

      return products
    } catch let error as PurchaseKitError {
      throw PurchaseKitError(
        code: error.code,
        message: error.message
      )
    } catch let error as StoreKitError {
      throw PurchaseKitError(
        code: ErrorCode.FAILED,
        message: error.localizedDescription
      )
    } catch {
      throw PurchaseKitError(
        code: ErrorCode.UNKNOWN,
        message: "An error happened when getting the products"
      )
    }
  }

  @MainActor
  func entitlements() async throws -> [Transaction] {
    var transactions: [Transaction] = []

    for await transaction in Transaction.currentEntitlements {
      switch transaction {
      case .verified(let transaction):
        transactions.append(transaction)

      case .unverified:
        continue
      }
    }

    return transactions
  }

  @MainActor
  func purchase(id: String, userId: String) async throws -> Transaction? {
    var options: Set<Product.PurchaseOption> = []
    let uuid = UUID(uuidString: userId)

    if let uuid = uuid {
      options = [.appAccountToken(uuid)]
    }

    guard let product = availableProducts.first(where: { product in
      product.id == id
    }) else {
      throw PurchaseKitError(
        code: ErrorCode.NOT_FOUND,
        message: "No product found for this id"
      )
    }

    let result = try await product.purchase(options: options)

    switch result {
    case .pending:
      throw PurchaseKitError(
        code: ErrorCode.PENDING,
        message: "Purchase is pending"
      )

    case .success(let verification):
      switch verification {
      case .verified(let transaction):
        if !purchasedProducts.contains(transaction) {
          purchasedProducts.insert(transaction)
        }

        return transaction

      case .unverified:
        throw PurchaseKitError(
          code: ErrorCode.UNVERIFIED,
          message: "Purchase failed StoreKit validation"
        )
      }

    case .userCancelled:
      throw PurchaseKitError(
        code: ErrorCode.CANCELLED,
        message: "User cancelled the purchase"
      )

    default:
      throw PurchaseKitError(
        code: ErrorCode.UNKNOWN,
        message: "An error happened when purchasing this product"
      )
    }
  }

  @MainActor
  func finishTransaction(id: String) async throws {
    do {
      guard let transaction = purchasedProducts.first(where: { product in
        product.productID == id
      }) else {
        throw PurchaseKitError(
          code: ErrorCode.NOT_FOUND,
          message: "No transaction to finish found for this id"
        )
      }

      await transaction.finish()

      purchasedProducts.remove(transaction)
    } catch {
      throw PurchaseKitError(
        code: ErrorCode.FAILED,
        message: "Transaction couldn't be finished"
      )
    }
  }

  @MainActor
  func syncPurchases() async -> Bool {
    return ((try? await AppStore.sync()) != nil)
  }
}
