import Foundation
import StoreKit
import Observation

// MARK: - State Types

enum DonationLoadState {
    case loading
    case loaded([Product])
    case empty
    case failed(String)
}

enum DonationPurchaseState: Equatable {
    case idle
    case purchasing(productID: String)
    case thankYou(productID: String)
    case pending(productID: String)
    case failed(productID: String, message: String)

    var activePurchasingID: String? {
        if case .purchasing(let id) = self { return id }
        return nil
    }

    var thankYouID: String? {
        if case .thankYou(let id) = self { return id }
        return nil
    }

    var pendingID: String? {
        if case .pending(let id) = self { return id }
        return nil
    }

    func errorMessage(for productID: String) -> String? {
        if case .failed(let id, let msg) = self, id == productID { return msg }
        return nil
    }

    var isIdle: Bool {
        if case .idle = self { return true }
        return false
    }
}

// MARK: - DonationService

/// Manages product loading, purchases, and donation history for the Support section.
/// Product IDs must exactly match App Store Connect configuration.
@Observable
@MainActor
final class DonationService {

    static let shared = DonationService()

    /// The exact product IDs as configured in App Store Connect.
    static let productIDs: Set<String> = [
        "doneatepage",
        "donatebook",
        "donateencyclopaedia"
    ]

    private(set) var loadState: DonationLoadState = .loading
    private(set) var purchaseState: DonationPurchaseState = .idle
    /// Maps productID → date of the most recent completed donation.
    private(set) var donationHistory: [String: Date] = [:]

    private var transactionListenerTask: Task<Void, Never>?
    private static let historyDefaultsKey = "bookstax.donationHistory"
    private static let nudgeDateKey = "bookstax.lastNudgeDate"
    private static let installDateKey = "bookstax.installDate"
    /// ~6 months in seconds
    private static let nudgeIntervalSeconds: TimeInterval = 182 * 24 * 3600
    /// 3-day grace period after install before first nudge
    private static let gracePeriodSeconds: TimeInterval = 3 * 24 * 3600

    private init() {
        donationHistory = Self.loadPersistedHistory()
        transactionListenerTask = startTransactionListener()
        // Record install date on first launch
        if UserDefaults.standard.object(forKey: Self.installDateKey) == nil {
            UserDefaults.standard.set(Date(), forKey: Self.installDateKey)
        }
    }

    // MARK: - Nudge & Supporter State

    /// True if the user has completed at least one donation.
    var hasEverDonated: Bool {
        !donationHistory.isEmpty
    }

    /// True if the nudge sheet should be presented.
    /// Returns false immediately once any donation has been made.
    var shouldShowNudge: Bool {
        guard !hasEverDonated else { return false }
        if let last = UserDefaults.standard.object(forKey: Self.nudgeDateKey) as? Date {
            return Date().timeIntervalSince(last) >= Self.nudgeIntervalSeconds
        }
        // Never shown before — only show after 3-day grace period
        guard let installDate = UserDefaults.standard.object(forKey: Self.installDateKey) as? Date else {
            return false
        }
        return Date().timeIntervalSince(installDate) >= Self.gracePeriodSeconds
    }

    /// Call when the nudge sheet is dismissed so we know when to show it next.
    func recordNudgeSeen() {
        UserDefaults.standard.set(Date(), forKey: Self.nudgeDateKey)
    }

    // MARK: - Product Loading

    func loadProducts() async {
        loadState = .loading
        do {
            let fetched = try await Product.products(for: Self.productIDs)
            loadState = fetched.isEmpty
                ? .empty
                : .loaded(fetched.sorted { $0.price < $1.price })
        } catch {
            AppLog(.error, "Product load failed: \(error)", category: "IAP")
            loadState = .failed(error.localizedDescription)
        }
    }

    // MARK: - Purchase

    func purchase(_ product: Product) async {
        guard case .idle = purchaseState else { return }
        purchaseState = .purchasing(productID: product.id)
        do {
            let result = try await product.purchase()
            await handlePurchaseResult(result, productID: product.id)
        } catch {
            AppLog(.error, "Purchase failed for \(product.id): \(error)", category: "IAP")
            purchaseState = .failed(productID: product.id, message: error.localizedDescription)
        }
    }

    func dismissError(for productID: String) {
        if case .failed(let id, _) = purchaseState, id == productID {
            purchaseState = .idle
        }
    }

    // MARK: - Private Purchase Handling

    private func handlePurchaseResult(_ result: Product.PurchaseResult, productID: String) async {
        switch result {
        case .success(let verification):
            switch verification {
            case .verified(let transaction):
                await completeTransaction(transaction)
                await showThankYou(for: productID)
            case .unverified(_, let error):
                AppLog(.error, "Unverified transaction for \(productID): \(error)", category: "IAP")
                purchaseState = .failed(productID: productID, message: error.localizedDescription)
            }
        case .userCancelled:
            purchaseState = .idle
        case .pending:
            // Deferred purchase (e.g. Ask to Buy). Resolved via transaction listener.
            purchaseState = .pending(productID: productID)
        @unknown default:
            purchaseState = .idle
        }
    }

    private func completeTransaction(_ transaction: Transaction) async {
        recordDonation(productID: transaction.productID)
        await transaction.finish()
        AppLog(.info, "Transaction \(transaction.id) finished for \(transaction.productID)", category: "IAP")
    }

    private func showThankYou(for productID: String) async {
        purchaseState = .thankYou(productID: productID)
        try? await Task.sleep(for: .seconds(3))
        if case .thankYou(let id) = purchaseState, id == productID {
            purchaseState = .idle
        }
    }

    // MARK: - Transaction Listener

    /// Listens for transaction updates from StoreKit (e.g. Ask to Buy approvals,
    /// transactions completed on other devices, interrupted purchases).
    private func startTransactionListener() -> Task<Void, Never> {
        Task.detached(priority: .background) { [weak self] in
            for await result in Transaction.updates {
                await self?.handleTransactionUpdate(result)
            }
        }
    }

    private func handleTransactionUpdate(_ result: VerificationResult<Transaction>) async {
        switch result {
        case .verified(let transaction):
            await completeTransaction(transaction)
            // Resolve a pending state caused by Ask to Buy or interrupted purchase.
            if case .pending(let id) = purchaseState, id == transaction.productID {
                await showThankYou(for: transaction.productID)
            }
        case .unverified(let transaction, let error):
            AppLog(.warning, "Unverified transaction update for \(transaction.productID): \(error)", category: "IAP")
        }
    }

    // MARK: - Donation History Persistence

    private func recordDonation(productID: String) {
        donationHistory[productID] = Date()
        Self.persistHistory(donationHistory)
    }

    private static func loadPersistedHistory() -> [String: Date] {
        guard
            let data = UserDefaults.standard.data(forKey: historyDefaultsKey),
            let decoded = try? JSONDecoder().decode([String: Date].self, from: data)
        else { return [:] }
        return decoded
    }

    private static func persistHistory(_ history: [String: Date]) {
        guard let data = try? JSONEncoder().encode(history) else { return }
        UserDefaults.standard.set(data, forKey: historyDefaultsKey)
    }
}
