//
//  ZcashRustBackendWelding.swift
//  ZcashLightClientKit
//
//  Created by Francisco Gindre on 12/09/2019.
//  Copyright © 2019 Electric Coin Company. All rights reserved.
//

import Foundation

enum RustWeldingError: Error {
    case genericError(message: String)
    case dataDbInitFailed(message: String)
    case dataDbNotEmpty
    case saplingSpendParametersNotFound
    case malformedStringInput
    case noConsensusBranchId(height: Int32)
    case unableToDeriveKeys
    case invalidInput(message: String)
    case invalidRewind(suggestedHeight: Int32)
}

enum ZcashRustBackendWeldingConstants {
    static let validChain: Int32 = -1
}
/// Enumeration of potential return states for database initialization. If `seedRequired`
/// is returned, the caller must re-attempt initialization providing the seed
public enum DbInitResult {
    case success
    case seedRequired
}

protocol ZcashRustBackendWelding {
    /// Adds the next available account-level spend authority, given the current set of [ZIP 316]
    /// account identifiers known, to the wallet database.
    ///
    /// Returns the newly created [ZIP 316] account identifier, along with the binary encoding of the
    /// [`UnifiedSpendingKey`] for the newly created account.  The caller should manage the memory of
    /// (and store) the returned spending keys in a secure fashion.
    ///
    /// If `seed` was imported from a backup and this method is being used to restore a
    /// previous wallet state, you should use this method to add all of the desired
    /// accounts before scanning the chain from the seed's birthday height.
    ///
    /// By convention, wallets should only allow a new account to be generated after funds
    /// have been received by the currently-available account (in order to enable
    /// automated account recovery).
    /// - Parameters:
    ///    - dbData: location of the data db
    ///    - seed: byte array of the zip32 seed
    ///    - networkType: network type of this key
    /// - Returns: The `UnifiedSpendingKey` structs for the number of accounts created
    ///
    static func createAccount(
        dbData: URL,
        seed: [UInt8],
        networkType: NetworkType
    ) throws -> UnifiedSpendingKey

    /// Creates a transaction to the given address from the given account
    /// - Parameter dbData: URL for the Data DB
    /// - Parameter usk: `UnifiedSpendingKey` for the account that controls the funds to be spent.
    /// - Parameter to: recipient address
    /// - Parameter value: transaction amount in Zatoshi
    /// - Parameter memo: the `MemoBytes` for this transaction. pass `nil` when sending to transparent receivers
    /// - Parameter spendParamsPath: path escaped String for the filesystem locations where the spend parameters are located
    /// - Parameter outputParamsPath: path escaped String for the filesystem locations where the output parameters are located
    /// - Parameter networkType: network type of this key
    static func createToAddress(
        dbData: URL,
        usk: UnifiedSpendingKey,
        to address: String,
        value: Int64,
        memo: MemoBytes?,
        spendParamsPath: String,
        outputParamsPath: String,
        networkType: NetworkType
    ) -> Int64 // swiftlint:disable function_parameter_count

    /// Scans a transaction for any information that can be decrypted by the accounts in the
    /// wallet, and saves it to the wallet.
    ///
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - tx:     the transaction to decrypt
    ///   - minedHeight: height on which this transaction was mined. this is used to fetch the consensus branch ID.
    ///   - networkType: network type of this key
    /// returns false if fails to decrypt.
    static func decryptAndStoreTransaction(
        dbData: URL,
        txBytes: [UInt8],
        minedHeight: Int32,
        networkType: NetworkType
    ) -> Bool

     /// Derives and returns a unified spending key from the given seed for the given account ID.
     /// Returns the binary encoding of the spending key. The caller should manage the memory of (and store, if necessary) the returned spending key in a secure fashion.
    /// - Parameter seed: a Byte Array with the seed
    /// - Parameter accountIndex:account index that the key can spend from
    /// - Parameter networkType: network type of this key
    /// - Throws `.unableToDerive` when there's an error
    static func deriveUnifiedSpendingKey(
        from seed: [UInt8],
        accountIndex: Int32,
        networkType: NetworkType
    ) throws -> UnifiedSpendingKey

    /// get the (unverified) balance from the given account
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - account: index of the given account
    ///   - networkType: network type of this key
    static func getBalance(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) -> Int64

    /// Returns the most-recently-generated unified payment address for the specified account.
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - account: index of the given account
    ///   - networkType: network type of this key
    static func getCurrentAddress(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) throws -> UnifiedAddress

    /// Wallets might need to be rewound because of a reorg, or by user request.
    /// There are times where the wallet could get out of sync for many reasons and
    /// users might be asked to rescan their wallets in order to fix that. This function
    /// returns the nearest height where a rewind is possible. Currently pruning gets rid
    /// of sapling witnesses older than 100 blocks. So in order to reconstruct the witness
    /// tree that allows to spend notes from the given wallet the rewind can't be more than
    /// 100 blocks or back to the oldest unspent note that this wallet contains.
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - height: height you would like to rewind to.
    ///   - networkType: network type of this key]
    /// - Returns: the blockheight of the nearest rewind height.
    ///
    static func getNearestRewindHeight(
        dbData: URL,
        height: Int32,
        networkType: NetworkType
    ) -> Int32

    /// Returns a newly-generated unified payment address for the specified account, with the next available diversifier.
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - account: index of the given account
    ///   - networkType: network type of this key
    static func getNextAvailableAddress(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) throws -> UnifiedAddress

    /// get received memo from note
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - idNote: note_id of note where the memo is located
    ///   - networkType: network type of this key
    @available(*, deprecated, message: "This function will be deprecated soon. Use `getReceivedMemo(dbData:idNote:networkType)` instead")
    static func getReceivedMemoAsUTF8(
        dbData: URL,
        idNote: Int64,
        networkType: NetworkType
    ) -> String?


    /// get received memo from note
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - idNote: note_id of note where the memo is located
    ///   - networkType: network type of this key
    static func getReceivedMemo(
        dbData: URL,
        idNote: Int64,
        networkType: NetworkType
    ) -> Memo?

    /// Returns the Sapling receiver within the given Unified Address, if any.
    /// - Parameter uAddr: a `UnifiedAddress`
    /// - Returns a `SaplingAddress` if any
    /// - Throws `receiverNotFound` when the receiver is not found. `invalidUnifiedAddress` if the UA provided is not valid
    static func getSaplingReceiver(for uAddr: UnifiedAddress) throws -> SaplingAddress?

    /// get sent memo from note
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - idNote: note_id of note where the memo is located
    ///   - networkType: network type of this key
    @available(*, deprecated, message: "This function will be deprecated soon. Use `getSentMemo(dbData:idNote:networkType)` instead")
    static func getSentMemoAsUTF8(
        dbData: URL,
        idNote: Int64,
        networkType: NetworkType
    ) -> String?

    /// get sent memo from note
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - idNote: note_id of note where the memo is located
    ///   - networkType: network type of this key
    /// - Returns: a `Memo` if any
    static func getSentMemo(
        dbData: URL,
        idNote: Int64,
        networkType: NetworkType
    ) -> Memo?

    /// Get the verified cached transparent balance for the given address
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - account; the account index to query
    ///   - networkType: network type of this key
    static func getTransparentBalance(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) throws -> Int64

    /// Returns the transparent receiver within the given Unified Address, if any.
    // - Parameter uAddr: a `UnifiedAddress`
    /// - Returns a `TransparentAddress` if any
    /// - Throws `receiverNotFound` when the receiver is not found. `invalidUnifiedAddress` if the UA provided is not valid
    static func getTransparentReceiver(for uAddr: UnifiedAddress) throws -> TransparentAddress?

    /// gets the latest error if available. Clear the existing error
    ///  - Returns a `RustWeldingError` if exists
    static func lastError() -> RustWeldingError?

    /// gets the latest error message from librustzcash. Does not clear existing error
    static func getLastError() -> String?

    /// initialize the accounts table from a set of unified full viewing keys
    /// - Note: this function should only be used when restoring an existing seed phrase.
    /// when creating a new wallet, use `createAccount()` instead
    /// - Parameter dbData: location of the data db
    /// - Parameter ufvks: an array of UnifiedFullViewingKeys
    /// - Parameter networkType: network type of this key
    static func initAccountsTable(
        dbData: URL,
        ufvks: [UnifiedFullViewingKey],
        networkType: NetworkType
    ) throws

    /// initializes the data db. This will performs any migrations needed on the sqlite file
    /// provided. Some migrations might need that callers provide the seed bytes.
    /// - Parameter dbData: location of the data db sql file
    /// - Parameter seed: ZIP-32 compliant seed bytes for this wallet
    /// - Parameter networkType: network type of this key
    /// - Returns: `DbInitResult.success` if the dataDb was initialized successfully
    /// or `DbInitResult.seedRequired` if the operation requires the seed to be passed
    /// in order to be completed successfully.
    static func initDataDb(
        dbData: URL,
        seed: [UInt8]?,
        networkType: NetworkType
    ) throws -> DbInitResult

    /// Returns the network and address type for the given Zcash address string,
    /// if the string represents a valid Zcash address.
    static func getAddressMetadata(_ address: String) -> AddressMetadata? 

    /// Validates the if the given string is a valid Sapling Address
    /// - Parameter address: UTF-8 encoded String to validate
    /// - Parameter networkType: network type of this key
    /// - Returns: true when the address is valid. Returns false in any other case
    /// - Throws: Error when the provided address belongs to another network
    static func isValidSaplingAddress(_ address: String, networkType: NetworkType) -> Bool

    /// Validates the if the given string is a valid Sapling Extended Full Viewing Key
    /// - Parameter key: UTF-8 encoded String to validate
    /// - Parameter networkType: network type of this key
    /// - Returns: `true` when the Sapling Extended Full Viewing Key is valid. `false` in any other case
    /// - Throws: Error when there's another problem not related to validity of the string in question
    static func isValidSaplingExtendedFullViewingKey(_ key: String, networkType: NetworkType) -> Bool

    /// Validates the if the given string is a valid Sapling Extended Spending Key
    /// - Returns: `true` when the Sapling Extended Spending Key is valid, false in any other case.
    /// - Throws: Error when the key is semantically valid  but it belongs to another network
    /// - parameter key: String encoded Extendeed Spending Key
    /// - parameter networkType: `NetworkType` signaling testnet or mainnet
    static func isValidSaplingExtendedSpendingKey(_ key: String, networkType: NetworkType) -> Bool

    /// Validates the if the given string is a valid Transparent Address
    /// - Parameter address: UTF-8 encoded String to validate
    /// - Parameter networkType: network type of this key
    /// - Returns: true when the address is valid and transparent. false in any other case
    /// - Throws: Error when the provided address belongs to another network
    static func isValidTransparentAddress(_ address: String, networkType: NetworkType) -> Bool

    /// validates whether a string encoded address is a valid Unified Address.
    /// - Parameter address: UTF-8 encoded String to validate
    /// - Parameter networkType: network type of this key
    /// - Returns: true when the address is valid and transparent. false in any other case
    /// - Throws: Error when the provided address belongs to another network
    static func isValidUnifiedAddress(_ address: String, networkType: NetworkType) -> Bool

    ///  verifies that the given string-encoded `UnifiedFullViewingKey` is valid.
    /// - Parameter ufvk: UTF-8 encoded String to validate
    /// - Parameter networkType: network type of this key
    /// - Returns: true when the encoded string is a valid UFVK. false in any other case
    /// - Throws: Error when there's another problem not related to validity of the string in question
    static func isValidUnifiedFullViewingKey(_ ufvk: String, networkType: NetworkType)  -> Bool

    /// initialize the blocks table from a given checkpoint (heigh, hash, time, saplingTree and networkType)
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - height: represents the block height of the given checkpoint
    ///   - hash: hash of the merkle tree
    ///   - time: in milliseconds from reference
    ///   - saplingTree: hash of the sapling tree
    ///   - networkType: `NetworkType` signaling testnet or mainnet
    static func initBlocksTable(
        dbData: URL,
        height: Int32,
        hash: String,
        time: UInt32,
        saplingTree: String,
        networkType: NetworkType
    ) throws // swiftlint:disable function_parameter_count

    /// Returns a list of the transparent receivers for the diversified unified addresses that have
    /// been allocated for the provided account.
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - account: index of the given account
    ///   - networkType: the network type
    static func listTransparentReceivers(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) throws -> [TransparentAddress]

    /// get the verified balance from the given account
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - account: index of the given account
    ///   - networkType: the network type
    static func getVerifiedBalance(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) -> Int64


    /// Get the verified cached transparent balance for the given account
    /// - Parameters:
    ///   - dbData: location of the data db
    ///   - account: account index to query the balance for.
    ///   - networkType: the network type
    static func getVerifiedTransparentBalance(
        dbData: URL,
        account: Int32,
        networkType: NetworkType
    ) throws -> Int64

    /// Checks that the scanned blocks in the data database, when combined with the recent
    /// `CompactBlock`s in the cache database, form a valid chain.
    /// This function is built on the core assumption that the information provided in the
    /// cache database is more likely to be accurate than the previously-scanned information.
    /// This follows from the design (and trust) assumption that the `lightwalletd` server
    /// provides accurate block information as of the time it was requested.
    /// - Parameters:
    ///   - dbCache: location of the cache db file
    ///   - dbData: location of the data db file
    ///   - networkType: the network type
    /// - Returns:
    ///  - `-1` if the combined chain is valid.
    ///  - `upper_bound` if the combined chain is invalid.
    ///  - `upper_bound` is the height of the highest invalid block (on the assumption that the highest block in the cache database is correct).
    ///  - `0` if there was an error during validation unrelated to chain validity.
    /// - Important: This function does not mutate either of the databases.
    static func validateCombinedChain(
        dbCache: URL,
        dbData: URL,
        networkType: NetworkType
    ) -> Int32

    /// Resets the state of the database to only contain block and transaction information up to the given height. clears up all derived data as well
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - height: height to rewind to. DON'T PASS ARBITRARY HEIGHT. Use getNearestRewindHeight when unsure
    ///   - networkType: the network type
    static func rewindToHeight(
        dbData: URL,
        height: Int32,
        networkType: NetworkType
    ) -> Bool
    

    /// Scans new blocks added to the cache for any transactions received by the tracked
    /// accounts.
    /// This function pays attention only to cached blocks with heights greater than the
    /// highest scanned block in `db_data`. Cached blocks with lower heights are not verified
    /// against previously-scanned blocks. In particular, this function **assumes** that the
    /// caller is handling rollbacks.
    /// For brand-new light client databases, this function starts scanning from the Sapling
    /// activation height. This height can be fast-forwarded to a more recent block by calling
    /// [`initBlocksTable`] before this function.
    /// Scanned blocks are required to be height-sequential. If a block is missing from the
    /// cache, an error will be signalled.
    ///
    /// - Parameters:
    ///   - dbCache: location of the compact block cache db
    ///   - dbData:  location of the data db file
    ///   - limit: scan up to limit blocks. pass 0 to set no limit.
    ///   - networkType: the network type
    /// returns false if fails to scan.
    static func scanBlocks(
        dbCache: URL,
        dbData: URL,
        limit: UInt32,
        networkType: NetworkType
    ) -> Bool


    /// puts a UTXO into the data db database
    /// - Parameters:
    ///   - dbData: location of the data db file
    ///   - txid: the txid bytes for the UTXO
    ///   - index: the index of the UTXO
    ///   - script: the script of the UTXO
    ///   - value: the value of the UTXO
    ///   - height: the mined height for the UTXO
    ///   - networkType: the network type
    /// - Returns: true if the operation succeded or false otherwise
    static func putUnspentTransparentOutput(
        dbData: URL,
        txid: [UInt8],
        index: Int,
        script: [UInt8],
        value: Int64,
        height: BlockHeight,
        networkType: NetworkType
    ) throws -> Bool

    /// Creates a transaction to shield all found UTXOs in cache db for the account the provided `UnifiedSpendingKey` has spend authority for.
    /// - Parameter dbCache: URL for the Cache DB
    /// - Parameter dbData: URL for the Data DB
    /// - Parameter usk: `UnifiedSpendingKey` that spend transparent funds and where the funds will be shielded to.
    /// - Parameter memo: the `Memo` for this transaction
    /// - Parameter spendParamsPath: path escaped String for the filesystem locations where the spend parameters are located
    /// - Parameter outputParamsPath: path escaped String for the filesystem locations where the output parameters are located
    /// - Parameter networkType: the network type
    static func shieldFunds(
        dbCache: URL,
        dbData: URL,
        usk: UnifiedSpendingKey,
        memo: MemoBytes?,
        spendParamsPath: String,
        outputParamsPath: String,
        networkType: NetworkType
    ) -> Int64 // swiftlint:disable function_parameter_count

    /// Obtains the available receiver typecodes for the given String encoded Unified Address
    /// - Parameter address: public key represented as a String
    /// - Returns  the `[UInt32]` that compose the given UA
    /// - Throws `RustWeldingError.invalidInput(message: String)` when the UA is either invalid or malformed
    static func receiverTypecodesOnUnifiedAddress(_ address: String) throws -> [UInt32]


    /// Gets the consensus branch id for the given height
    /// - Parameter height: the height you what to know the branch id for
    /// - Parameter networkType: the network type
    static func consensusBranchIdFor(
        height: Int32,
        networkType: NetworkType
    ) throws -> Int32


    /// Derives a `UnifiedFullViewingKey` from a `UnifiedSpendingKey`
    /// - Parameter spendingKey: the `UnifiedSpendingKey` to derive from
    /// - Parameter networkType: the network type
    /// - Throws: `RustWeldingError.unableToDeriveKeys` if the SDK couldn't derive the UFVK.
    /// - Returns: the derived `UnifiedFullViewingKey`
    static func deriveUnifiedFullViewingKey(
        from spendingKey: UnifiedSpendingKey,
        networkType: NetworkType
    ) throws -> UnifiedFullViewingKey
}
