//
//  TransactionEntity.swift
//  PirateLightClientKit
//
//  Created by Francisco Gindre on 11/14/19.
//

import Foundation
import SQLite

public enum ZcashTransaction {
    public struct Overview {
        /// Represents the transaction state based on current height of the chain,
        /// mined height and expiry height of a transaction.
        public enum State {
            /// transaction has a `minedHeight` that's greater or equal than
            /// `PirateSDK.defaultStaleTolerance` confirmations.
            case confirmed
            /// transaction has no `minedHeight` but current known height is less than
            /// `expiryHeight`.
            case pending
            /// transaction has no
            case expired

            init(
                currentHeight: BlockHeight,
                minedHeight: BlockHeight?,
                expiredUnmined: Bool
            ) {
                guard !expiredUnmined else {
                    self = .expired
                    return
                }

                if let minedHeight, (currentHeight - minedHeight) >= PirateSDK.defaultStaleTolerance {
                    self = .confirmed
                } else if let minedHeight, (currentHeight - minedHeight) < PirateSDK.defaultStaleTolerance {
                    self = .pending
                } else if minedHeight == nil {
                    self = .pending
                } else {
                    self = .expired
                }
            }
        }

        public let accountId: Int
        public let blockTime: TimeInterval?
        public let expiryHeight: BlockHeight?
        public let fee: Zatoshi?
        public let id: Int
        public let index: Int?
        public var isSentTransaction: Bool { value < Zatoshi(0) }
        public let hasChange: Bool
        public let memoCount: Int
        public let minedHeight: BlockHeight?
        public let raw: Data?
        public let rawID: Data
        public let receivedNoteCount: Int
        public let sentNoteCount: Int
        public let value: Zatoshi
        public let isExpiredUmined: Bool
    }

    public struct Output {
        public enum Pool {
            case transaparent
            case sapling
            case other(Int)
            init(rawValue: Int) {
                switch rawValue {
                case 0:
                    self = .transaparent
                case 2:
                    self = .sapling
                default:
                    self = .other(rawValue)
                }
            }
        }

        public let idTx: Int
        public let pool: Pool
        public let index: Int
        public let fromAccount: Int?
        public let recipient: TransactionRecipient
        public let value: Zatoshi
        public let isChange: Bool
        public let memo: Memo?
    }

    /// Used when fetching blocks from the lightwalletd
    struct Fetched {
        public let rawID: Data
        public let minedHeight: BlockHeight
        public let raw: Data
    }
}

extension ZcashTransaction.Output {
    enum Column {
        static let idTx = SQLite.Expression<Int>("id_tx")
        static let pool = SQLite.Expression<Int>("output_pool")
        static let index = SQLite.Expression<Int>("output_index")
        static let toAccount = SQLite.Expression<Int?>("to_account")
        static let fromAccount = SQLite.Expression<Int?>("from_account")
        static let toAddress = SQLite.Expression<String?>("to_address")
        static let value = SQLite.Expression<Int64>("value")
        static let isChange = SQLite.Expression<Bool>("is_change")
        static let memo = SQLite.Expression<Blob?>("memo")
    }

    init(row: Row) throws {
        do {
            idTx = try row.get(Column.idTx)
            pool = .init(rawValue: try row.get(Column.pool))
            index = try row.get(Column.index)
            fromAccount = try row.get(Column.fromAccount)
            value = Zatoshi(try row.get(Column.value))
            isChange = try row.get(Column.isChange)

            if
                let outputRecipient = try row.get(Column.toAddress),
                let metadata = DerivationTool.getAddressMetadata(outputRecipient)
            {
                recipient = TransactionRecipient.address(try Recipient(outputRecipient, network: metadata.networkType))
            } else if let toAccount = try row.get(Column.toAccount) {
                recipient = .internalAccount(UInt32(toAccount))
            } else {
                throw ZcashError.zcashTransactionOutputInconsistentRecipient
            }

            if let memoData = try row.get(Column.memo) {
                memo = try Memo(bytes: memoData.bytes)
            } else {
                memo = nil
            }
        } catch {
            throw ZcashError.zcashTransactionOutputInit(error)
        }
    }
}

extension ZcashTransaction.Overview {
    enum Column {
        static let accountId = SQLite.Expression<Int>("account_id")
        static let id = SQLite.Expression<Int>("id_tx")
        static let minedHeight = SQLite.Expression<BlockHeight?>("mined_height")
        static let index = SQLite.Expression<Int?>("tx_index")
        static let rawID = SQLite.Expression<Blob>("txid")
        static let expiryHeight = SQLite.Expression<BlockHeight?>("expiry_height")
        static let raw = SQLite.Expression<Blob?>("raw")
        static let value = SQLite.Expression<Int64>("account_balance_delta")
        static let fee = SQLite.Expression<Int64?>("fee_paid")
        static let hasChange = SQLite.Expression<Bool>("has_change")
        static let sentNoteCount = SQLite.Expression<Int>("sent_note_count")
        static let receivedNoteCount = SQLite.Expression<Int>("received_note_count")
        static let memoCount = SQLite.Expression<Int>("memo_count")
        static let blockTime = SQLite.Expression<Int64?>("block_time")
        static let expiredUnmined = SQLite.Expression<Bool>("expired_unmined")
    }

    init(row: Row) throws {
        do {
            self.accountId = try row.get(Column.accountId)
            self.expiryHeight = try row.get(Column.expiryHeight)
            self.id = try row.get(Column.id)
            self.index = try row.get(Column.index)
            self.hasChange = try row.get(Column.hasChange)
            self.memoCount = try row.get(Column.memoCount)
            self.minedHeight = try row.get(Column.minedHeight)
            self.rawID = Data(blob: try row.get(Column.rawID))
            self.receivedNoteCount = try row.get(Column.receivedNoteCount)
            self.sentNoteCount = try row.get(Column.sentNoteCount)
            self.value = Zatoshi(try row.get(Column.value))
            self.isExpiredUmined = try row.get(Column.expiredUnmined)

            if let blockTime = try row.get(Column.blockTime) {
                self.blockTime = TimeInterval(blockTime)
            } else {
                self.blockTime = nil
            }

            if let fee = try row.get(Column.fee) {
                self.fee = Zatoshi(fee)
            } else {
                self.fee = nil
            }

            if let raw = try row.get(Column.raw) {
                self.raw = Data(blob: raw)
            } else {
                self.raw = nil
            }
        } catch {
            throw ZcashError.zcashTransactionOverviewInit(error)
        }
    }

    func anchor(network: PirateNetwork) -> BlockHeight? {
        guard let minedHeight = self.minedHeight else { return nil }
        if minedHeight != -1 {
            return max(minedHeight - PirateSDK.defaultStaleTolerance, network.constants.saplingActivationHeight)
        }

        guard let expiryHeight = self.expiryHeight else { return nil }
        if expiryHeight != -1 {
            return max(expiryHeight - PirateSDK.expiryOffset - PirateSDK.defaultStaleTolerance, network.constants.saplingActivationHeight)
        }

        return nil
    }
}

/// extension to handle pending states
public extension ZcashTransaction.Overview {
    func getState(for currentHeight: BlockHeight) -> State {
        State(
            currentHeight: currentHeight,
            minedHeight: minedHeight,
            expiredUnmined: self.isExpiredUmined
        )
    }

    func isPending(currentHeight: BlockHeight) -> Bool {
        getState(for: currentHeight) == .pending
    }
}

/**
Capabilities of an entity that can be uniquely identified by a raw transaction id
*/
public protocol RawIdentifiable {
    var rawTransactionId: Data? { get set }
}
