import Foundation
import CommonCrypto
import CashuDevKit
import ZeusCashuRestore

/// CashuDevKit Native Module for React Native
/// Provides bridge to CDK FFI bindings
@objc(CashuDevKitModule)
class CashuDevKitModule: RCTEventEmitter {

    // MARK: - Properties

    private var wallet: MultiMintWallet?
    private var db: WalletSqliteDatabase?
    private var preparedSends: [String: PreparedSend] = [:]
    private var isInitialized: Bool = false

    // Serial queue for thread-safe wallet access
    private let walletQueue = DispatchQueue(label: "app.zeusln.cashudevkit.wallet")

    // MARK: - Module Setup

    @objc
    override static func moduleName() -> String! {
        return "CashuDevKitModule"
    }

    @objc
    override static func requiresMainQueueSetup() -> Bool {
        return false
    }

    // MARK: - Helper Methods

    /// Parse P2PK spending conditions from JSON dictionary
    private func parseP2PKConditions(from json: [String: Any]) -> SpendingConditions? {
       guard let kind = json["kind"] as? String,
          kind == "P2PK",
          let condData = json["data"] as? [String: Any],
          let pubkey = condData["pubkey"] as? String,
          !pubkey.isEmpty else {
        return nil
    }
    
      let locktime: UInt64 = {
        if let lt = condData["locktime"] as? NSNumber {
            return lt.uint64Value
        }
        return 0
    }()
    
    let refundKeys: [String] = {
        if let keys = condData["refund_keys"] as? [String] {
            return keys.filter { !$0.isEmpty }
        }
        return []
    }()
    
    let conditions = Conditions(
        locktime: locktime,
        pubkeys: [],
        refundKeys: refundKeys,
        numSigs: 0,
        sigFlag: 0,
        numSigsRefund: 0
    )
    
    return .p2pk(pubkey: pubkey, conditions: conditions)
  }

    private func readPositiveUInt64(from json: [String: Any], key: String) -> UInt64 {
        guard let raw = json[key] else {
            return 0
        }

        if raw is Bool {
            return 0
        }

        if let number = raw as? NSNumber {
            let value = number.int64Value
            return value > 0 ? UInt64(value) : 0
        }

        if let string = raw as? String,
           let value = UInt64(string),
           value > 0 {
            return value
        }

        return 0
    }

    private func parseMeltOptions(from optionsJson: String?) -> MeltOptions? {
        guard
            let json = optionsJson,
            let data = json.data(using: .utf8),
            let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
        else {
            return nil
        }

        if let mpp = parsed["mpp"] as? [String: Any] {
            let amount = readPositiveUInt64(from: mpp, key: "amount")
            if amount > 0 {
                return .mpp(amount: Amount(value: amount))
            }
        }

        if let amountless = parsed["amountless"] as? [String: Any] {
            let amountMsat = readPositiveUInt64(from: amountless, key: "amount_msat")
            if amountMsat > 0 {
                return .amountless(
                    amountMsat: Amount(value: amountMsat)
                )
            }
        }

        return nil
    }

    /// Returns the initialized wallet or rejects with NO_WALLET error
    private func getInitializedWallet(reject: @escaping RCTPromiseRejectBlock) -> MultiMintWallet? {
        guard isInitialized, let wallet = wallet else {
            reject("NO_WALLET", "Wallet not initialized", nil)
            return nil
        }
        return wallet
    }

    private var currentDbPath: String?

    private func getDatabasePath(for mnemonic: String) -> String {
        let paths = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
        let appSupport = paths[0]

        // Ensure directory exists
        try? FileManager.default.createDirectory(at: appSupport, withIntermediateDirectories: true)

        // Hash the mnemonic to create a unique, deterministic filename per wallet
        let data = Data(mnemonic.utf8)
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        _ = data.withUnsafeBytes { CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) }
        let hashHex = hash.prefix(8).map { String(format: "%02x", $0) }.joined()

        let dbPath = appSupport.appendingPathComponent("cashu_wallet_\(hashHex).db")
        return dbPath.path
    }

    private func encodeToJson(_ object: Any) -> String? {
        guard let data = try? JSONSerialization.data(withJSONObject: object) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }

    private func parseCurrencyUnit(_ unit: String) -> CurrencyUnit {
        switch unit.lowercased() {
        case "sat":
            return .sat
        case "msat":
            return .msat
        case "usd":
            return .usd
        case "eur":
            return .eur
        default:
            return .sat
        }
    }

    private func quoteStateToString(_ state: QuoteState) -> String {
        switch state {
        case .unpaid:
            return "Unpaid"
        case .paid:
            return "Paid"
        case .pending:
            return "Pending"
        case .issued:
            return "Issued"
        @unknown default:
            return "Unknown"
        }
    }

    private func currencyUnitToString(_ unit: CurrencyUnit) -> String {
        switch unit {
        case .sat:
            return "sat"
        case .msat:
            return "msat"
        case .usd:
            return "usd"
        case .eur:
            return "eur"
        case .auth:
            return "auth"
        case .custom(let unit):
            return unit
        @unknown default:
            return "sat"
        }
    }

    private func mapFfiError(_ error: FfiError) -> (code: String, message: String) {
        switch error {
        case .Generic(let message):
            return ("GENERIC_ERROR", message)
        case .AmountOverflow(let message):
            return ("AMOUNT_OVERFLOW", message)
        case .PaymentFailed(let message):
            return ("PAYMENT_FAILED", message)
        case .PaymentPending(let message):
            return ("PAYMENT_PENDING", message)
        case .InsufficientFunds(let message):
            return ("INSUFFICIENT_FUNDS", message)
        case .Database(let message):
            return ("DATABASE_ERROR", message)
        case .Network(let message):
            return ("NETWORK_ERROR", message)
        case .InvalidToken(let message):
            return ("INVALID_TOKEN", message)
        case .Wallet(let message):
            return ("WALLET_ERROR", message)
        case .KeysetUnknown(let message):
            return ("KEYSET_UNKNOWN", message)
        case .UnitNotSupported(let message):
            return ("UNIT_NOT_SUPPORTED", message)
        case .InvalidMnemonic(let message):
            return ("INVALID_MNEMONIC", message)
        case .InvalidUrl(let message):
            return ("INVALID_URL", message)
        case .DivisionByZero(let message):
            return ("DIVISION_BY_ZERO", message)
        case .Amount(let message):
            return ("AMOUNT_ERROR", message)
        case .RuntimeTaskJoin(let message):
            return ("RUNTIME_ERROR", message)
        case .InvalidHex(let message):
            return ("INVALID_HEX", message)
        case .InvalidCryptographicKey(let message):
            return ("INVALID_KEY", message)
        case .Serialization(let message):
            return ("SERIALIZATION_ERROR", message)
        @unknown default:
            return ("UNKNOWN_ERROR", "Unknown error occurred")
        }
    }

    private func encodeMintQuote(_ quote: MintQuote) -> [String: Any] {
        return [
            "id": quote.id,
            "amount": quote.amount?.value ?? 0,
            "unit": currencyUnitToString(quote.unit),
            "request": quote.request,
            "state": quoteStateToString(quote.state),
            "expiry": quote.expiry,
            "mint_url": quote.mintUrl.url
        ]
    }

    private func encodeMeltQuote(_ quote: MeltQuote) -> [String: Any] {
        var result: [String: Any] = [
            "id": quote.id,
            "amount": quote.amount.value,
            "unit": currencyUnitToString(quote.unit),
            "request": quote.request,
            "fee_reserve": quote.feeReserve.value,
            "state": quoteStateToString(quote.state),
            "expiry": quote.expiry
        ]
        if let preimage = quote.paymentPreimage {
            result["payment_preimage"] = preimage
        }
        return result
    }

    private func encodeMelted(_ melted: Melted) -> [String: Any] {
        var result: [String: Any] = [
            "state": quoteStateToString(melted.state),
            "amount": melted.amount.value,
            "fee_paid": melted.feePaid.value
        ]
        if let preimage = melted.preimage {
            result["preimage"] = preimage
        }
        if let change = melted.change {
            result["change"] = change.map { encodeProof($0) }
        }
        return result
    }

    private func encodeProof(_ proof: Proof) -> [String: Any] {
        let result: [String: Any] = [
            "amount": proof.amount.value,
            "secret": proof.secret,
            "c": proof.c,
            "keyset_id": proof.keysetId
        ]
        // Witness and DLEQ are optional
        return result
    }

    private func encodeToken(_ token: Token) async throws -> [String: Any] {
        let value = try token.value()
        let mintUrl = try token.mintUrl()
        var result: [String: Any] = [
        "encoded": token.encode(),
        "value": value.value,
        "mint_url": mintUrl.url,
        "memo": token.memo() ?? "",
        "unit": token.unit().map { currencyUnitToString($0) } ?? "sat"
    ]
    do {
        if let wallet = self.wallet {
            let keysets = try await wallet.getMintKeysets(mintUrl: mintUrl)
            let proofs = try token.proofs(mintKeysets: keysets)
            result["proofs"] = proofs.map { encodeProof($0) }
        }
    } catch {
        result["proofs"] = []
    }
    return result
    }

    private func encodeMintInfo(_ info: MintInfo) -> [String: Any] {
        var result: [String: Any] = [:]

        if let name = info.name {
            result["name"] = name
        }
        if let pubkey = info.pubkey {
            result["pubkey"] = pubkey
        }
        if let version = info.version {
            result["version"] = version
        }
        if let description = info.description {
            result["description"] = description
        }
        if let descriptionLong = info.descriptionLong {
            result["description_long"] = descriptionLong
        }
        if let motd = info.motd {
            result["motd"] = motd
        }

        return result
    }

    private func encodeKeyset(_ keyset: KeySetInfo) -> [String: Any] {
        return [
            "id": keyset.id,
            "unit": currencyUnitToString(keyset.unit),
            "active": keyset.active,
            "input_fee_ppk": keyset.inputFeePpk
        ]
    }

    // MARK: - Database Path

    @objc(getDatabasePath:rejecter:)
    func getDatabasePath(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
        resolve(currentDbPath ?? "")
    }

    // MARK: - Wallet Management

    @objc(initializeWallet:unit:resolver:rejecter:)
    func initializeWallet(_ mnemonic: String, unit: String,
                          resolve: @escaping RCTPromiseResolveBlock,
                          reject: @escaping RCTPromiseRejectBlock) {
        Task {
            do {
                let dbPath = getDatabasePath(for: mnemonic)
                self.currentDbPath = dbPath
                let sqliteDb = try WalletSqliteDatabase(filePath: dbPath)
                self.db = sqliteDb

                let currencyUnit = parseCurrencyUnit(unit)

                // WalletSqliteDatabase conforms to WalletDatabase protocol, pass directly
                let newWallet = try MultiMintWallet(
                    unit: currencyUnit,
                    mnemonic: mnemonic,
                    db: sqliteDb
                )

                walletQueue.sync {
                    self.wallet = newWallet
                    self.isInitialized = true
                }

                resolve(nil)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("INIT_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(addMint:targetProofCount:resolver:rejecter:)
    func addMint(_ mintUrl: String, targetProofCount: NSNumber,
                 resolve: @escaping RCTPromiseResolveBlock,
                 reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                // Use nil if targetProofCount is 0 or negative (sentinel for "use default")
                let count: UInt32? = targetProofCount.intValue > 0 ? targetProofCount.uint32Value : nil
                try await wallet.addMint(mintUrl: url, targetProofCount: count)
                resolve(nil)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("ADD_MINT_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(removeMint:resolver:rejecter:)
    func removeMint(_ mintUrl: String,
                    resolve: @escaping RCTPromiseResolveBlock,
                    reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                await wallet.removeMint(mintUrl: url)
                if let db = self.db {
                    try await db.removeMint(mintUrl: url)
                } else {
                    reject(
                        "DATABASE_NOT_INITIALIZED",
                        "Database not initialized",
                        nil
                    )
                    return
                }
                resolve(nil)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("REMOVE_MINT_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(getMintUrls:rejecter:)
    func getMintUrls(resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            let urls = await wallet.getMintUrls()
            resolve(urls)
        }
    }

    // MARK: - Balance Operations

    @objc(getTotalBalance:rejecter:)
    func getTotalBalance(resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let balances = try await wallet.getBalances()
                var total: UInt64 = 0
                for (_, amount) in balances {
                    total += amount.value
                }
                resolve(NSNumber(value: total))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("BALANCE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(getBalances:rejecter:)
    func getBalances(resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let balances = try await wallet.getBalances()
                var result: [String: UInt64] = [:]
                for (mintUrl, amount) in balances {
                    result[mintUrl] = amount.value
                }
                resolve(encodeToJson(result))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("BALANCE_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Mint Info

    @objc(fetchMintInfo:resolver:rejecter:)
    func fetchMintInfo(_ mintUrl: String,
                       resolve: @escaping RCTPromiseResolveBlock,
                       reject: @escaping RCTPromiseRejectBlock) {
        // fetchMintInfo uses direct HTTP - works without wallet initialization
        Task {
            do {
                // Normalize URL and construct info endpoint
                let normalizedUrl = mintUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
                let infoUrl = normalizedUrl + "/v1/info"

                guard let requestUrl = URL(string: infoUrl) else {
                    reject("INVALID_URL", "Invalid mint URL: \(mintUrl)", nil)
                    return
                }

                let (data, response) = try await URLSession.shared.data(from: requestUrl)

                // Check HTTP response status
                if let httpResponse = response as? HTTPURLResponse {
                    guard (200...299).contains(httpResponse.statusCode) else {
                        reject("HTTP_ERROR", "Mint returned HTTP \(httpResponse.statusCode)", nil)
                        return
                    }
                }

                // Parse JSON response
                guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                    reject("PARSE_ERROR", "Failed to parse mint info response", nil)
                    return
                }

                // Encode and resolve
                if let encoded = encodeToJson(json) {
                    resolve(encoded)
                } else {
                    reject("ENCODE_ERROR", "Failed to encode mint info", nil)
                }
            } catch {
                reject("NETWORK_ERROR", "Failed to fetch mint info: \(error.localizedDescription)", error)
            }
        }
    }

    @objc(getMintKeysets:resolver:rejecter:)
    func getMintKeysets(_ mintUrl: String,
                        resolve: @escaping RCTPromiseResolveBlock,
                        reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let keysets = try await wallet.getMintKeysets(mintUrl: url)
                let encoded = keysets.map { encodeKeyset($0) }
                resolve(encodeToJson(encoded))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("KEYSETS_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Mint Quotes (Receiving)

    @objc(createMintQuote:amount:description:resolver:rejecter:)
    func createMintQuote(_ mintUrl: String, amount: NSNumber, description: String?,
                         resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let amt = Amount(value: amount.uint64Value)
                let quote = try await wallet.mintQuote(mintUrl: url, amount: amt, description: description)
                resolve(encodeToJson(encodeMintQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("MINT_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(checkMintQuote:quoteId:resolver:rejecter:)
    func checkMintQuote(_ mintUrl: String, quoteId: String,
                        resolve: @escaping RCTPromiseResolveBlock,
                        reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let quote = try await wallet.checkMintQuote(mintUrl: url, quoteId: quoteId)
                resolve(encodeToJson(encodeMintQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("CHECK_MINT_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    /// Check mint quote status directly from the mint's HTTP API.
    /// This bypasses the local database check and works for external quotes
    /// (e.g., quotes created by ZeusPay server).
    @objc(checkExternalMintQuote:quoteId:resolver:rejecter:)
    func checkExternalMintQuote(_ mintUrl: String, quoteId: String,
                                 resolve: @escaping RCTPromiseResolveBlock,
                                 reject: @escaping RCTPromiseRejectBlock) {
        Task {
            do {
                // Normalize mint URL and construct the quote endpoint
                let normalizedUrl = mintUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
                let quoteUrlStr = "\(normalizedUrl)/v1/mint/quote/bolt11/\(quoteId)"

                guard let quoteUrl = URL(string: quoteUrlStr) else {
                    reject("INVALID_URL", "Invalid mint URL: \(mintUrl)", nil)
                    return
                }

                let (data, response) = try await URLSession.shared.data(from: quoteUrl)

                // Check HTTP response status
                if let httpResponse = response as? HTTPURLResponse {
                    guard (200...299).contains(httpResponse.statusCode) else {
                        let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error"
                        reject("HTTP_ERROR", "Mint returned HTTP \(httpResponse.statusCode): \(errorBody)", nil)
                        return
                    }
                }

                // Parse JSON response
                guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                    reject("PARSE_ERROR", "Failed to parse mint quote response", nil)
                    return
                }

                // Parse the response according to NUT-04 spec
                var result: [String: Any] = [
                    "id": json["quote"] as? String ?? quoteId,
                    "amount": json["amount"] as? Int ?? 0,
                    "request": json["request"] as? String ?? "",
                    "state": json["state"] as? String ?? "Unknown",
                    "expiry": json["expiry"] as? Int ?? 0,
                    "mint_url": mintUrl
                ]

                // Include pubkey if present (for P2PK locked quotes)
                if let pubkey = json["pubkey"] as? String {
                    result["pubkey"] = pubkey
                }

                if let encoded = encodeToJson(result) {
                    resolve(encoded)
                } else {
                    reject("ENCODE_ERROR", "Failed to encode quote response", nil)
                }
            } catch {
                reject("EXTERNAL_QUOTE_ERROR", "Failed to check external quote: \(error.localizedDescription)", error)
            }
        }
    }

    /// Add an external mint quote to CDK's database.
    /// This allows minting from quotes created externally (e.g., by ZeusPay server).
    @objc(addExternalMintQuote:quoteId:amount:request:state:expiry:secretKey:resolver:rejecter:)
    func addExternalMintQuote(_ mintUrl: String, quoteId: String, amount: NSNumber,
                               request: String, state: String, expiry: NSNumber,
                               secretKey: String?,
                               resolve: @escaping RCTPromiseResolveBlock,
                               reject: @escaping RCTPromiseRejectBlock) {
        guard isInitialized, let db = db else {
            reject("NO_WALLET", "Wallet not initialized", nil)
            return
        }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let amt = Amount(value: UInt64(truncating: amount))

                // Map state string to QuoteState enum
                let quoteState: QuoteState
                switch state.uppercased() {
                case "UNPAID":
                    quoteState = .unpaid
                case "PAID":
                    quoteState = .paid
                case "PENDING":
                    quoteState = .pending
                case "ISSUED":
                    quoteState = .issued
                default:
                    quoteState = .paid // Default to PAID for external quotes
                }

                // Create the MintQuote object
                // For external quotes that are PAID, we set amountPaid = amount
                // Include secretKey for P2PK-locked quotes
                let zeroAmount = Amount(value: 0)
                let quote = MintQuote(
                    id: quoteId,
                    amount: amt,
                    unit: .sat,
                    request: request,
                    state: quoteState,
                    expiry: UInt64(truncating: expiry),
                    mintUrl: url,
                    amountIssued: quoteState == .issued ? amt : zeroAmount,
                    amountPaid: (quoteState == .paid || quoteState == .issued) ? amt : zeroAmount,
                    paymentMethod: .bolt11,
                    secretKey: secretKey
                )

                // Add to database
                try await db.addMintQuote(quote: quote)

                resolve(true)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("ADD_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    /// Mint tokens directly from an external quote.
    /// This creates the quote in CDK's database first, then mints.
    @objc(mintExternal:quoteId:amount:resolver:rejecter:)
    func mintExternal(_ mintUrl: String, quoteId: String, amount: NSNumber,
                      resolve: @escaping RCTPromiseResolveBlock,
                      reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)

                // Mint - quote should be in database now
                let proofs = try await wallet.mint(mintUrl: url, quoteId: quoteId, spendingConditions: nil)
                let encoded = proofs.map { encodeProof($0) }
                resolve(encodeToJson(encoded))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("MINT_EXTERNAL_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(mint:quoteId:conditionsJson:resolver:rejecter:)
    func mint(_ mintUrl: String, quoteId: String, conditionsJson: String?,
              resolve: @escaping RCTPromiseResolveBlock,
              reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)

                // Parse spending conditions if provided
                var conditions: SpendingConditions? = nil
                if let json = conditionsJson,
                   let data = json.data(using: .utf8),
                   let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
                   conditions = parseP2PKConditions(from: parsed)
                }

                let proofs = try await wallet.mint(mintUrl: url, quoteId: quoteId, spendingConditions: conditions)
                let encoded = proofs.map { encodeProof($0) }
                resolve(encodeToJson(encoded))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("MINT_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Melt Quotes (Paying)

    @objc(createMeltQuote:request:optionsJson:resolver:rejecter:)
    func createMeltQuote(_ mintUrl: String, request: String, optionsJson: String?,
                         resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let options = parseMeltOptions(from: optionsJson)
                let quote = try await wallet.meltQuote(
                    mintUrl: url,
                    request: request,
                    options: options
                )
                resolve(encodeToJson(encodeMeltQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("MELT_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(checkMeltQuote:quoteId:resolver:rejecter:)
    func checkMeltQuote(_ mintUrl: String, quoteId: String,
                        resolve: @escaping RCTPromiseResolveBlock,
                        reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let quote = try await wallet.checkMeltQuote(mintUrl: url, quoteId: quoteId)
                resolve(encodeToJson(encodeMeltQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("CHECK_MELT_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(melt:quoteId:resolver:rejecter:)
    func melt(_ mintUrl: String, quoteId: String,
              resolve: @escaping RCTPromiseResolveBlock,
              reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let melted = try await wallet.meltWithMint(mintUrl: url, quoteId: quoteId)
                resolve(encodeToJson(encodeMelted(melted)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("MELT_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(meltMpp:optionsJson:maxFee:resolver:rejecter:)
    func meltMpp(_ bolt11: String, optionsJson: String?, maxFee: NSNumber,
                 resolve: @escaping RCTPromiseResolveBlock,
                 reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let options = parseMeltOptions(from: optionsJson)
                let fee = maxFee.uint64Value > 0 ? Amount(value: maxFee.uint64Value) : nil
                let melted = try await wallet.melt(bolt11: bolt11, options: options, maxFee: fee)
                resolve(encodeToJson(encodeMelted(melted)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("MELT_MPP_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(meltPartial:bolt11:mppAmountMsat:resolver:rejecter:)
    func meltPartial(_ mintUrl: String, bolt11: String, mppAmountMsat: NSNumber,
                     resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let mppAmount = Amount(value: mppAmountMsat.uint64Value)

                // Step 1: Create melt quote via CDK with MPP options
                let options = MeltOptions.mpp(amount: mppAmount)
                let quote = try await wallet.meltQuote(
                    mintUrl: url,
                    request: bolt11,
                    options: options
                )

                // Step 2: Select proofs via CDK and execute melt
                // Try meltProofs with all proofs from this mint —
                // the mint knows the MPP partial amount from the quote
                let allProofs = try await wallet.listProofs()
                let normalizedUrl = mintUrl.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
                var mintProofs: [Proof] = []
                for (key, proofs) in allProofs {
                    let normalizedKey = key.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
                    if normalizedKey == normalizedUrl {
                        mintProofs = proofs
                        break
                    }
                }

                guard !mintProofs.isEmpty else {
                    reject("NO_PROOFS", "No proofs found for mint \(mintUrl)", nil)
                    return
                }

                // Step 3: Try CDK's meltProofs first (keeps proof DB in sync)
                let melted = try await wallet.meltProofs(
                    mintUrl: url,
                    quoteId: quote.id,
                    proofs: mintProofs
                )

                resolve(encodeToJson(encodeMelted(melted)))
            } catch {
                reject("MELT_PARTIAL_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Token Operations

    @objc(prepareSend:amount:optionsJson:resolver:rejecter:)
    func prepareSend(_ mintUrl: String, amount: NSNumber, optionsJson: String?,
                     resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let amt = Amount(value: amount.uint64Value)

                // Parse options if provided
                var includeFee = false
                var spendingConditions: SpendingConditions? = nil
                var sendKind: SendKind = .onlineExact
                if let json = optionsJson,
                   let data = json.data(using: .utf8),
                   let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
                    if let fee = parsed["include_fee"] as? Bool {
                        includeFee = fee
                    }
                    if let cond = parsed["conditions"] as? [String: Any] {
                    spendingConditions = parseP2PKConditions(from: cond)
                    }
                    if let kindStr = parsed["send_kind"] as? String {
                        let tolerance = (parsed["tolerance"] as? NSNumber).map { Amount(value: $0.uint64Value) } ?? Amount(value: 0)
                        switch kindStr {
                        case "OfflineExact":
                            sendKind = .offlineExact
                        case "OnlineTolerance":
                            sendKind = .onlineTolerance(tolerance: tolerance)
                        case "OfflineTolerance":
                            sendKind = .offlineTolerance(tolerance: tolerance)
                        default:
                            sendKind = .onlineExact
                        }
                    }
                }

                let innerSendOptions = SendOptions(
                    memo: nil,
                    conditions: spendingConditions,
                    amountSplitTarget: .none,
                    sendKind: sendKind,
                    includeFee: includeFee,
                    maxProofs: nil,
                    metadata: [:]
                )
                let sendOptions = MultiMintSendOptions(
                    allowTransfer: false,
                    maxTransferAmount: nil,
                    allowedMints: [],
                    excludedMints: [],
                    sendOptions: innerSendOptions
                )

                let prepared = try await wallet.prepareSend(mintUrl: url, amount: amt, options: sendOptions)
                let preparedId = prepared.id()
                let preparedAmount = prepared.amount().value
                let preparedFee = prepared.fee().value

                walletQueue.sync {
                    self.preparedSends[preparedId] = prepared
                }

                let result: [String: Any] = [
                    "id": preparedId,
                    "amount": preparedAmount,
                    "fee": preparedFee
                ]
                let jsonData = try JSONSerialization.data(withJSONObject: result)
                resolve(String(data: jsonData, encoding: .utf8)!)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("PREPARE_SEND_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(confirmSend:memo:resolver:rejecter:)
    func confirmSend(_ preparedSendId: String, memo: String?,
                     resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
        guard let prepared = preparedSends[preparedSendId] else {
            reject("NO_PREPARED_SEND", "Prepared send not found", nil)
            return
        }

        Task {
            defer {
                // Always clean up prepared send, whether success or failure
                _ = walletQueue.sync {
                    self.preparedSends.removeValue(forKey: preparedSendId)
                }
            }

            do {
                let token = try await prepared.confirm(memo: memo)
                let encoded = try await encodeToken(token)
                resolve(encodeToJson(encoded))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("CONFIRM_SEND_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(cancelSend:resolver:rejecter:)
    func cancelSend(_ preparedSendId: String,
                    resolve: @escaping RCTPromiseResolveBlock,
                    reject: @escaping RCTPromiseRejectBlock) {
        guard let prepared = preparedSends[preparedSendId] else {
            resolve(nil)
            return
        }

        Task {
            defer {
                // Always clean up prepared send, whether success or failure
                _ = walletQueue.sync {
                    self.preparedSends.removeValue(forKey: preparedSendId)
                }
            }

            do {
                try await prepared.cancel()
                resolve(nil)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("CANCEL_SEND_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(receive:optionsJson:resolver:rejecter:)
    func receive(_ encodedToken: String, optionsJson: String?,
                 resolve: @escaping RCTPromiseResolveBlock,
                 reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let token = try Token.fromString(encodedToken: encodedToken)

                // Parse options if provided
                var p2pkSigningKeys: [SecretKey] = []
                var preimages: [String] = []

                if let json = optionsJson,
                   let data = json.data(using: .utf8),
                   let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
                    if let keys = parsed["p2pk_signing_keys"] as? [String] {
                        p2pkSigningKeys = keys.compactMap {
                            SecretKey(hex: $0)
                        }
                    }
                    if let imgs = parsed["preimages"] as? [String] {
                        preimages = imgs
                    }
                }

                let innerReceiveOptions = ReceiveOptions(
                    amountSplitTarget: .none,
                    p2pkSigningKeys: p2pkSigningKeys,
                    preimages: preimages,
                    metadata: [:]
                )
                let receiveOptions = MultiMintReceiveOptions(
                    allowUntrusted: true,
                    transferToMint: nil,
                    receiveOptions: innerReceiveOptions
                )

                let amount = try await wallet.receive(token: token, options: receiveOptions)
                resolve(NSNumber(value: amount.value))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("RECEIVE_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Token Utility

    @objc(decodeToken:resolver:rejecter:)
    func decodeToken(_ encodedToken: String,
                     resolve: @escaping RCTPromiseResolveBlock,
                     reject: @escaping RCTPromiseRejectBlock) {
    Task {
        do {
            let token = try Token.fromString(encodedToken: encodedToken)
            let encoded = try await encodeToken(token)
            resolve(encodeToJson(encoded))
        } catch let error as FfiError {
            let (code, message) = mapFfiError(error)
            reject(code, message, error)
        } catch {
            reject("DECODE_ERROR", error.localizedDescription, error)
        }
     }
    }

    @objc(isValidToken:resolver:rejecter:)
    func isValidToken(_ encodedToken: String,
                      resolve: @escaping RCTPromiseResolveBlock,
                      reject: @escaping RCTPromiseRejectBlock) {
        do {
            let _ = try Token.fromString(encodedToken: encodedToken)
            resolve(true)
        } catch {
            resolve(false)
        }
    }

    // MARK: - Restore

    @objc(restore:resolver:rejecter:)
    func restore(_ mintUrl: String,
                 resolve: @escaping RCTPromiseResolveBlock,
                 reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let amount = try await wallet.restore(mintUrl: url)
                resolve(NSNumber(value: amount.value))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("RESTORE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(restoreFromSeed:seedHex:resolver:rejecter:)
    func restoreFromSeed(_ mintUrl: String, seedHex: String,
                         resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                // Step 1: Call standalone restore crate to get v1 proofs as a cashu token
                let tokenString = try ZeusCashuRestore.restoreFromSeed(mintUrl: mintUrl, seedHex: seedHex)

                // If no proofs found, return 0
                if tokenString.isEmpty {
                    resolve(NSNumber(value: 0))
                    return
                }

                // Step 2: Feed the token into CDK's receive to import proofs into the wallet
                let token = try Token.fromString(encodedToken: tokenString)

                let innerReceiveOptions = ReceiveOptions(
                    amountSplitTarget: .none,
                    p2pkSigningKeys: [],
                    preimages: [],
                    metadata: [:]
                )
                let receiveOptions = MultiMintReceiveOptions(
                    allowUntrusted: true,
                    transferToMint: nil,
                    receiveOptions: innerReceiveOptions
                )

                let amount = try await wallet.receive(token: token, options: receiveOptions)
                resolve(NSNumber(value: amount.value))
            } catch let error as ZeusCashuRestore.RestoreError {
                reject("RESTORE_FROM_SEED_ERROR", "\(error)", nil)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("RESTORE_FROM_SEED_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Proof Management

    @objc(checkProofsState:proofsJson:resolver:rejecter:)
    func checkProofsState(_ mintUrl: String, proofsJson: String,
                          resolve: @escaping RCTPromiseResolveBlock,
                          reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)

                // Parse proofs from JSON
                guard let data = proofsJson.data(using: .utf8),
                      let proofsArray = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
                    reject("INVALID_PROOFS", "Could not parse proofs JSON", nil)
                    return
                }

                var proofs: [Proof] = []
                for proofDict in proofsArray {
                    guard let amount = proofDict["amount"] as? UInt64,
                          let secret = proofDict["secret"] as? String,
                          let c = proofDict["c"] as? String,
                          let keysetId = proofDict["keyset_id"] as? String else {
                        continue
                    }

                    let proof = Proof(
                        amount: Amount(value: amount),
                        secret: secret,
                        c: c,
                        keysetId: keysetId,
                        witness: nil,
                        dleq: nil
                    )
                    proofs.append(proof)
                }

                let states = try await wallet.checkProofsState(mintUrl: url, proofs: proofs)
                let encoded = states.map { state -> [String: Any] in
                    var stateStr: String
                    switch state {
                    case .unspent:
                        stateStr = "Unspent"
                    case .pending:
                        stateStr = "Pending"
                    case .spent:
                        stateStr = "Spent"
                    default:
                        stateStr = "Unknown"
                    }
                    return ["state": stateStr]
                }
                resolve(encodeToJson(encoded))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("CHECK_PROOFS_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - BOLT12 Support

    @objc(createMintBolt12Quote:amount:description:resolver:rejecter:)
    func createMintBolt12Quote(_ mintUrl: String, amount: NSNumber, description: String?,
                               resolve: @escaping RCTPromiseResolveBlock,
                               reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let amt = Amount(value: amount.uint64Value)

                // Note: BOLT12 mint quote might need different API - check CDK version
                let quote = try await wallet.mintQuote(mintUrl: url, amount: amt, description: description)
                resolve(encodeToJson(encodeMintQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("BOLT12_MINT_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(createMeltBolt12Quote:request:optionsJson:resolver:rejecter:)
    func createMeltBolt12Quote(_ mintUrl: String, request: String, optionsJson: String?,
                               resolve: @escaping RCTPromiseResolveBlock,
                               reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let options = parseMeltOptions(from: optionsJson)
                let quote = try await wallet.meltQuote(
                    mintUrl: url,
                    request: request,
                    options: options
                )
                resolve(encodeToJson(encodeMeltQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("BOLT12_MELT_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(createMeltHumanReadableQuote:address:amountMsat:resolver:rejecter:)
    func createMeltHumanReadableQuote(_ mintUrl: String, address: String, amountMsat: NSNumber,
                                       resolve: @escaping RCTPromiseResolveBlock,
                                       reject: @escaping RCTPromiseRejectBlock) {
        guard let wallet = getInitializedWallet(reject: reject) else { return }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let quote = try await wallet.meltHumanReadableQuote(
                    mintUrl: url,
                    address: address,
                    amountMsat: amountMsat.uint64Value
                )
                resolve(encodeToJson(encodeMeltQuote(quote)))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("HUMAN_READABLE_QUOTE_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Transactions

    @objc(listTransactions:resolver:rejecter:)
    func listTransactions(_ direction: String?,
                          resolve: @escaping RCTPromiseResolveBlock,
                          reject: @escaping RCTPromiseRejectBlock) {
        guard isInitialized, let _ = wallet, let db = db else {
            reject("NO_WALLET", "Wallet not initialized", nil)
            return
        }

        Task {
            do {
                var txDirection: TransactionDirection? = nil
                if let dir = direction {
                    txDirection = dir == "incoming" ? .incoming : .outgoing
                }

                let transactions = try await db.listTransactions(
                    mintUrl: nil,
                    direction: txDirection,
                    unit: nil
                )

                let encoded = transactions.map { tx -> [String: Any] in
                    var result: [String: Any] = [
                        "id": tx.id.hex,
                        "direction": tx.direction == .incoming ? "incoming" : "outgoing",
                        "amount": tx.amount.value,
                        "mint_url": tx.mintUrl.url,
                        "timestamp": tx.timestamp,
                        "fee": tx.fee.value
                    ]
                    if let memo = tx.memo {
                        result["memo"] = memo
                    }
                    return result
                }

                resolve(encodeToJson(encoded))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("LIST_TRANSACTIONS_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Direct Proof Access (Offline Send)

    @objc(getUnspentProofs:resolver:rejecter:)
    func getUnspentProofs(_ mintUrl: String,
                          resolve: @escaping RCTPromiseResolveBlock,
                          reject: @escaping RCTPromiseRejectBlock) {
        guard let db = self.db else {
            reject("NO_WALLET", "Wallet not initialized", nil)
            return
        }

        Task {
            do {
                let url = MintUrl(url: mintUrl)
                let proofInfos = try await db.getProofs(
                    mintUrl: url,
                    unit: .sat,
                    state: [.unspent],
                    spendingConditions: nil
                )

                let result = proofInfos.map { info -> [String: Any] in
                    return [
                        "amount": info.proof.amount.value,
                        "secret": info.proof.secret,
                        "c": info.proof.c,
                        "keyset_id": info.proof.keysetId,
                        "y": info.y.hex
                    ]
                }

                resolve(encodeToJson(result))
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("GET_PROOFS_ERROR", error.localizedDescription, error)
            }
        }
    }

    @objc(removeProofs:resolver:rejecter:)
    func removeProofs(_ proofsYJson: String,
                      resolve: @escaping RCTPromiseResolveBlock,
                      reject: @escaping RCTPromiseRejectBlock) {
        guard let db = self.db else {
            reject("NO_WALLET", "Wallet not initialized", nil)
            return
        }

        Task {
            do {
                guard let data = proofsYJson.data(using: .utf8),
                      let yStrings = try? JSONSerialization.jsonObject(with: data) as? [String] else {
                    reject("INVALID_INPUT", "Could not parse Y values JSON", nil)
                    return
                }

                if yStrings.isEmpty {
                    resolve(nil)
                    return
                }

                let ys = yStrings.map { hex -> CashuDevKit.PublicKey in
                    CashuDevKit.PublicKey(hex: hex)
                }

                try await db.updateProofs(added: [], removedYs: ys)
                resolve(nil)
            } catch let error as FfiError {
                let (code, message) = mapFfiError(error)
                reject(code, message, error)
            } catch {
                reject("REMOVE_PROOFS_ERROR", error.localizedDescription, error)
            }
        }
    }

    // MARK: - Cleanup

    override func invalidate() {
        walletQueue.sync {
            wallet = nil
            db = nil
            preparedSends.removeAll()
            isInitialized = false
        }
        super.invalidate()
    }
}
