//
//  TipJarView.swift
//  Tusker
//
//  Created by Shadowfacts on 1/24/23.
//  Copyright © 2023 Shadowfacts. All rights reserved.
//

import SwiftUI
import StoreKit
import Combine

struct TipJarView: View {
    private static let productIDs = [
        "tusker.tip.small",
        "tusker.tip.medium",
        "tusker.tip.large",
    ]
    
    @State private var isLoaded = false
    @State private var products: [(Product, Bool)] = []
    @State private var error: Error?
    @State private var showConfetti = false
    @State private var updatesObserver: Task<Void, Never>?
    @State private var buttonWidth: CGFloat?
    @StateObject private var observer = UbiquitousKeyValueStoreObserver()
    
    var body: some View {
        ZStack {
            Color.appGroupedBackground
                .edgesIgnoringSafeArea(.all)
            
            productsView
            
            if showConfetti {
                ConfettiView()
                    .transition(.opacity.animation(.default))
            }
        }
        .navigationTitle("Tip Jar")
        .alertWithData("Error", data: $error, actions: { _ in
            Button("OK") {}
        }, message: { error in
            Text(error.localizedDescription)
        })
        .task {
            updatesObserver = Task.detached {
                await observeTransactionUpdates()
            }
            do {
                products = try await Product.products(for: Self.productIDs).map { ($0, false) }
                products.sort(by: { $0.0.price < $1.0.price })
                isLoaded = true
            } catch {
                self.error = .fetchingProducts(error)
            }
        }
        .onDisappear {
            updatesObserver?.cancel()
        }
        .onReceive(Just(showConfetti).filter { $0 }.delay(for: .seconds(5), scheduler: DispatchQueue.main)) { _ in
            showConfetti = false
        }
    }
    
    @ViewBuilder
    private var productsView: some View {
        if isLoaded {
            VStack {
                Text("If you're enjoying using Tusker and want to show your gratitude or help support its development, it is greatly appreciated!")
                    .multilineTextAlignment(.center)
                    .padding(.horizontal)
                
                VStack(alignment: .myAlignment) {
                    ForEach($products, id: \.0.id) { $productAndPurchasing in
                        TipRow(product: productAndPurchasing.0, buttonWidth: buttonWidth, isPurchasing: $productAndPurchasing.1, showConfetti: $showConfetti)
                    }
                }
                .onPreferenceChange(ButtonWidthKey.self) { newValue in
                    if let buttonWidth {
                        self.buttonWidth = max(buttonWidth, newValue)
                    } else {
                        self.buttonWidth = newValue
                    }
                }
                
                if let total = getTotalTips(), total > 0 {
                    Text("You've tipped a total of \(Text(total, format: products[0].0.priceFormatStyle)) 😍")
                    Text("Thank you!")
                }
            }
        } else {
            ProgressView()
                .progressViewStyle(.circular)
        }
    }
    
    private func observeTransactionUpdates() async {
        for await verificationResult in StoreKit.Transaction.updates {
            guard let index = products.firstIndex(where: { $0.0.id == verificationResult.unsafePayloadValue.productID }) else {
                continue
            }
            switch verificationResult {
            case .verified(let transaction):
                await transaction.finish()
                self.products[index].1 = false
                self.showConfetti = true
            case .unverified(_, let error):
                self.error = .verifyingTransaction(error)
            }
        }
    }
    
}

extension TipJarView {
    enum Error {
        case fetchingProducts(Swift.Error)
        case purchasing(Swift.Error)
        case verifyingTransaction(VerificationResult<StoreKit.Transaction>.VerificationError)
        
        var localizedDescription: String {
            switch self {
            case .fetchingProducts(let underlying):
                return "Error fetching products: \(String(describing: underlying))"
            case .purchasing(let underlying):
                return "Error purchasing: \(String(describing: underlying))"
            case .verifyingTransaction(let underlying):
                return "Error verifying transaction: \(String(describing: underlying))"
            }
        }
    }
}

private struct TipRow: View {
    let product: Product
    let buttonWidth: CGFloat?
    @Binding var isPurchasing: Bool
    @Binding var showConfetti: Bool
    @State private var error: TipJarView.Error?
    
    var body: some View {
        HStack {
            Text(product.displayName)
                .alignmentGuide(.myAlignment, computeValue: { context in context[.trailing] })
            
            Button {
                Task {
                    await self.purchase()
                }
            } label: {
                if isPurchasing {
                    ProgressView()
                        .progressViewStyle(.circular)
                        .frame(width: buttonWidth, alignment: .center)
                } else {
                    Text(product.displayPrice)
                        .background(GeometryReader { proxy in
                            Color.clear
                                .preference(key: ButtonWidthKey.self, value: proxy.size.width)
                        })
                        .frame(width: buttonWidth)
                }
            }
            .buttonStyle(.borderedProminent)
        }
        .alertWithData("Error", data: $error) { _ in
            Button("OK") {}
        } message: { error in
            Text(error.localizedDescription)
        }
    }
    
    private func purchase() async {
        isPurchasing = true
        let result: Product.PurchaseResult
        do {
            result = try await product.purchase()
        } catch {
            self.error = .purchasing(error)
            isPurchasing = false
            return
        }
        
        let transaction: StoreKit.Transaction
        switch result {
        case .success(let verificationResult):
            switch verificationResult {
            case .unverified(_, let reason):
                isPurchasing = false
                error = .verifyingTransaction(reason)
                return
            case .verified(let t):
                transaction = t
            }
        case .userCancelled:
            isPurchasing = false
            return
        case .pending:
            // pending transactions may still be completed, but we won't update UI in response to them
            isPurchasing = false
            return
        @unknown default:
            isPurchasing = false
            return
        }
        await transaction.finish()
        isPurchasing = false
        showConfetti = true
        addToTotalTips(amount: product.price)
    }
}

extension HorizontalAlignment {
    private enum MyTrailing: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context[.bottom]
        }
    }
    fileprivate static let myAlignment = HorizontalAlignment(MyTrailing.self)
}

private struct ButtonWidthKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = max(value, nextValue())
    }
}

private func getTotalTips() -> Decimal? {
    if let data = NSUbiquitousKeyValueStore.default.data(forKey: "totalTips"),
       let existing = try? PropertyListDecoder().decode(Decimal.self, from: data) {
        return existing
    } else {
        return nil
    }
}

private func addToTotalTips(amount: Decimal) {
    let newAmount = amount + (getTotalTips() ?? 0)
    if let data = try? PropertyListEncoder().encode(newAmount) {
        NSUbiquitousKeyValueStore.default.set(data, forKey: "totalTips")
    }
}

private class UbiquitousKeyValueStoreObserver: ObservableObject {
    private var cancellable: AnyCancellable?
    init() {
        self.cancellable = NotificationCenter.default.publisher(for: NSUbiquitousKeyValueStore.didChangeExternallyNotification)
            .receive(on: DispatchQueue.main)
            .sink { [unowned self] _ in
                self.objectWillChange.send()
            }
    }
}
