import Foundation
import StoreKit

/// Errors raised locally by `StoreKitPurchaseService` itself (as opposed to
/// errors StoreKit hands back verbatim, e.g. from `VerificationResult`'s
/// `.unverified` case, which are re-thrown as-is). Kept in the same
/// `Error, LocalizedError` + German `errorDescription` shape as `APIError`
/// in `Networking/APIError.swift` so both surface consistently in the UI.
enum StoreKitPurchaseError: Error, LocalizedError {
    /// `Product.products(for:)` returned nothing for the requested
    /// identifier — misconfigured product ID, or not yet propagated in App
    /// Store Connect / the local `Configuration.storekit` file.
    case productNotFound

    var errorDescription: String? {
        switch self {
        case .productNotFound:
            return "Dieses Produkt ist aktuell nicht verfügbar. Bitte versuche es später erneut."
        }
    }
}

/// Real StoreKit 2 implementation of `PurchaseService` (see
/// `Features/Settings/PurchaseService.swift` for the protocol and the
/// App Store Review Guideline 3.1.1 background on why this exists at all).
///
/// NOTE ON SERVER-SIDE ENTITLEMENT SYNC: this class only talks to StoreKit /
/// Apple's servers. It does **not** update `users.plan` on the backend — see
/// the long comment on `updatesTask` below, this is the important caveat to
/// understand before assuming a successful purchase here means the account
/// is actually Pro.
final class StoreKitPurchaseService: PurchaseService {

    /// Background listener for transactions that complete outside the
    /// synchronous `purchase()` call (renewals, refunds, Ask-to-Buy
    /// approvals, purchases made on another device). Started once, for the
    /// lifetime of this service instance.
    private let updatesTask: Task<Void, Never>

    init() {
        updatesTask = Task.detached {
            for await result in Transaction.updates {
                await StoreKitPurchaseService.handleUpdatedTransaction(result)
            }
        }
    }

    deinit {
        updatesTask.cancel()
    }

    // MARK: - PurchaseService

    func loadProducts() async throws -> [PurchaseProduct] {
        let products = try await Product.products(for: PurchaseProductID.all)

        let mapped: [PurchaseProduct] = products.compactMap { product in
            let planID: String
            switch product.id {
            case PurchaseProductID.weekly:
                planID = "weekly"
            case PurchaseProductID.annual:
                planID = "annual"
            case PurchaseProductID.lifetime:
                planID = "lifetime"
            default:
                // Unknown identifier (shouldn't happen — we only asked for
                // PurchaseProductID.all — but never silently invent a plan
                // for something we don't recognise).
                return nil
            }

            let periodDescription: String?
            if product.type == .nonConsumable {
                periodDescription = nil
            } else if let subscription = product.subscription {
                periodDescription = StoreKitPurchaseService.periodDescription(for: subscription.subscriptionPeriod)
            } else {
                periodDescription = nil
            }

            return PurchaseProduct(
                id: product.id,
                planID: planID,
                displayName: product.displayName,
                displayPrice: product.displayPrice,
                periodDescription: periodDescription
            )
        }

        // Render in a fixed, sensible order regardless of what the store
        // returns — PaywallView renders in the order we give back.
        let order = [PurchaseProductID.weekly, PurchaseProductID.annual, PurchaseProductID.lifetime]
        return mapped.sorted { lhs, rhs in
            let lhsIndex = order.firstIndex(of: lhs.id) ?? order.count
            let rhsIndex = order.firstIndex(of: rhs.id) ?? order.count
            return lhsIndex < rhsIndex
        }
    }

    func purchase(productID: String, appAccountToken: UUID?) async throws -> Bool {
        guard let product = try await Product.products(for: [productID]).first else {
            throw StoreKitPurchaseError.productNotFound
        }

        // Ties the resulting transaction back to this app's own user id so
        // the backend (POST /api/webhooks/apple) knows whose account to
        // grant Pro to — see `User.appleAccountToken`'s doc comment for
        // exactly how that round-trip works. Purchasing without it (nil)
        // still completes the purchase on Apple's side, it just can't be
        // reconciled server-side without a manual look-up later.
        var options: Set<Product.PurchaseOption> = []
        if let appAccountToken {
            options.insert(.appAccountToken(appAccountToken))
        }

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

        switch result {
        case .success(let verificationResult):
            switch verificationResult {
            case .verified(let transaction):
                await transaction.finish()
                return true
            case .unverified(_, let error):
                // StoreKit couldn't verify the transaction's signature (e.g.
                // jailbroken device, tampered receipt) — surface the
                // underlying error rather than treating it as a success.
                throw error
            }

        case .userCancelled:
            // Not an error — the user backed out of the purchase sheet.
            return false

        case .pending:
            // Ask-to-Buy / family approval pending. Nothing more to do
            // synchronously; the eventual approval (or denial) arrives later
            // via Transaction.updates, handled by `updatesTask` below.
            return false

        @unknown default:
            return false
        }
    }

    func restorePurchases() async throws {
        try await AppStore.sync()
    }

    // MARK: - Transaction updates listener

    /// Verifies and finishes a transaction observed via `Transaction.updates`.
    ///
    /// IMPORTANT — this is genuinely incomplete, documented honestly rather
    /// than hidden: finishing a transaction here only tells StoreKit "this
    /// purchase has been dealt with, stop re-presenting it" — it does
    /// **not** unlock Pro server-side. The backend has no
    /// `/api/webhooks/apple` endpoint yet to reconcile a StoreKit purchase
    /// into `users.plan` (see `app/ios/README.md`'s "What's deliberately NOT
    /// done yet" section) — that reconciliation is separate, not-yet-built
    /// backend work driven by Apple's server-to-server notifications. Do
    /// NOT be tempted to set some local "is Pro" flag here as a substitute:
    /// the account's real Pro status lives on the server (`users.plan`),
    /// and nothing in this file can change that yet.
    private static func handleUpdatedTransaction(_ result: VerificationResult<Transaction>) async {
        switch result {
        case .verified(let transaction):
            await transaction.finish()
        case .unverified:
            // Can't verify it; leave it alone rather than finishing a
            // transaction we couldn't authenticate. It will be re-delivered.
            break
        }
    }

    // MARK: - Formatting helpers

    /// Turns a `Product.SubscriptionPeriod` into a short German phrase, e.g.
    /// `(unit: .week, value: 1)` -> "pro Woche", `(unit: .year, value: 1)`
    /// -> "pro Jahr", `(unit: .month, value: 3)` -> "alle 3 Monate". This app
    /// currently only ever configures value == 1 periods (see
    /// `Configuration.storekit`), but the multi-value phrasing is filled in
    /// so this doesn't silently misrender if that ever changes.
    private static func periodDescription(for period: Product.SubscriptionPeriod) -> String {
        let value = period.value

        if value == 1 {
            switch period.unit {
            case .day:
                return "pro Tag"
            case .week:
                return "pro Woche"
            case .month:
                return "pro Monat"
            case .year:
                return "pro Jahr"
            @unknown default:
                return "pro Abrechnungszeitraum"
            }
        }

        switch period.unit {
        case .day:
            return "alle \(value) Tage"
        case .week:
            return "alle \(value) Wochen"
        case .month:
            return "alle \(value) Monate"
        case .year:
            return "alle \(value) Jahre"
        @unknown default:
            return "alle \(value) Abrechnungszeiträume"
        }
    }
}
