//
//  TestCoordinator.swift
//  ZcashLightClientKit-Unit-Tests
//
//  Created by Francisco Gindre on 4/29/20.
//

import Foundation
@testable import ZcashLightClientKit
@testable import Zircles
/**
 This is the TestCoordinator
 What does it do? quite a lot.
 Is it a nice "SOLID" "Clean Code" piece of source code?
 Hell no. It's your testing overlord and you will be grateful it is.
 */
class TestCoordinator {
    
    enum CoordinatorError: Error {
        case notDarksideWallet
        case notificationFromUnknownSynchronizer
        case notMockLightWalletService
    }
    
    enum SyncThreshold {
        case upTo(height: BlockHeight)
        case latestHeight
    }
    
    enum DarksideData {
        case `default`
        case predefined(dataset: DarksideDataset)
        case url(urlString: String, startHeigth: BlockHeight)
    }
    
    var completionHandler: ((SDKSynchronizer) -> Void)?
    var errorHandler: ((Error?) -> Void)?
    var seed: String
    var birthday: BlockHeight
    var channelProvider: ChannelProvider
    var synchronizer: SDKSynchronizer
    var service: DarksideWalletService
    var combineSynchronizer: CombineSynchronizer
    var spendingKeys: [String]?
    var databases: TemporaryTestDatabases
    
    init(seed: String,
         walletBirthday: BlockHeight,
         channelProvider: ChannelProvider) throws {
        self.seed = seed
        self.birthday = walletBirthday
        self.channelProvider = channelProvider
        self.databases = TemporaryDbBuilder.build()
        self.service = DarksideWalletService()
        let storage = CompactBlockStorage(url: databases.cacheDB, readonly: false)
        try storage.createTable()
        
        let downloader = CompactBlockDownloader(service: self.service, storage: storage)
        
        
        let buildResult = try TestSynchronizerBuilder.build(
                                rustBackend: ZcashRustBackend.self,
                                lowerBoundHeight: self.birthday,
                                cacheDbURL: databases.cacheDB,
                                dataDbURL: databases.dataDB,
                                pendingDbURL: databases.pendingDB,
                                service: self.service,
                                repository: TransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: databases.dataDB.absoluteString)),
                                downloader: downloader,
                                spendParamsURL: try __spendParamsURL(),
                                outputParamsURL: try __outputParamsURL(),
                                seedBytes: TestSeed().seed(),
                                walletBirthday: WalletBirthday.birthday(with: birthday),
                                loggerProxy: SampleLogger(logLevel: .debug))
        
        self.synchronizer = buildResult.synchronizer.synchronizer
        self.combineSynchronizer = buildResult.synchronizer
        self.spendingKeys = buildResult.spendingKeys
        subscribeToNotifications(synchronizer: self.synchronizer)
    }
    
    func stop() throws {
        try synchronizer.stop()
        self.completionHandler = nil
        self.errorHandler = nil
        
    }
    
    func setDarksideWalletState(_ state: DarksideData) throws {
       
        switch state {
        case .default:
            try service.useDataset(DarksideDataset.beforeReOrg.rawValue)
        case .predefined(let dataset):
            try service.useDataset(dataset.rawValue)
        case .url(let urlString,_):
            try service.useDataset(from: urlString)
        }
        
    }
    
    func setLatestHeight(height: BlockHeight) throws {
        try service.applyStaged(nextLatestHeight: height)
    }
    
    func sync(completion: @escaping (SDKSynchronizer) -> Void, error: @escaping (Error?) -> Void) throws {
        self.completionHandler = completion
        self.errorHandler = error
        
        try synchronizer.start()
    }
    
    
    /**
     Notifications
     */
    func subscribeToNotifications(synchronizer: Synchronizer) {
        
        NotificationCenter.default.addObserver(self, selector: #selector(synchronizerFailed(_:)), name: .synchronizerFailed, object: synchronizer)
        NotificationCenter.default.addObserver(self, selector: #selector(synchronizerSynced(_:)), name: .synchronizerSynced, object: synchronizer)
        
    }
    
    @objc func synchronizerFailed(_ notification: Notification) {
        self.errorHandler?(notification.userInfo?[SDKSynchronizer.NotificationKeys.error] as? Error)
    }
    
    @objc func synchronizerSynced(_ notification: Notification) {
        if case .stopped = self.synchronizer.status {
            LoggerProxy.debug("WARNING: notification received after synchronizer was stopped")
            return
        }
        self.completionHandler?(self.synchronizer)
    }
    
    @objc func synchronizerDisconnected(_ notification: Notification) {
        /// TODO: See if we need hooks for this
    }
    
    @objc func synchronizerStarted(_ notification: Notification) {
        /// TODO: See if we need hooks for this
    }
    
    @objc func synchronizerStopped(_ notification: Notification) {
        /// TODO: See if we need hooks for this
    }
    
    @objc func synchronizerSyncing(_ notification: Notification) {
        /// TODO: See if we need hooks for this
    }
}

extension TestCoordinator {
    func resetBlocks(dataset: DarksideData) throws {
    
        switch dataset {
        case .default:
            try service.useDataset(DarksideDataset.beforeReOrg.rawValue)
        case .predefined(let blocks):
            try service.useDataset(blocks.rawValue)
        case .url(let urlString,_):
            try service.useDataset(urlString)
        }
    }
    
    func stageBlockCreate(height: BlockHeight, count: Int = 1, nonce: Int = 0) throws {
        try service.stageBlocksCreate(from: height, count: count, nonce: 0)
    }
    
    func applyStaged(blockheight: BlockHeight) throws {
        try service.applyStaged(nextLatestHeight: blockheight)
    }
    
    func stageTransaction(_ tx: RawTransaction, at height: BlockHeight) throws {
        try service.stageTransaction(tx, at: height)
    }
    
    func stageTransaction(url: String, at height: BlockHeight) throws {
        try service.stageTransaction(from: url, at: height)
    }
    
    func latestHeight() throws -> BlockHeight {
        try service.latestBlockHeight()
    }
    
    func reset(saplingActivation: BlockHeight) throws {
        try service.reset(saplingActivation: saplingActivation)
    }
    
    func getIncomingTransactions() throws -> [RawTransaction]? {
        return try service.getIncomingTransactions()
    }
}

struct TemporaryTestDatabases {
       var cacheDB: URL
       var dataDB: URL
       var pendingDB: URL
}

class TemporaryDbBuilder {

    static func build() -> TemporaryTestDatabases {
        let tempUrl = try! __documentsDirectory()
        let timestamp = String(Int(Date().timeIntervalSince1970))
        
        return TemporaryTestDatabases(cacheDB: tempUrl.appendingPathComponent("cache_db_\(timestamp).db"),
                                      dataDB: tempUrl.appendingPathComponent("data_db_\(timestamp).db"),
                                      pendingDB: tempUrl.appendingPathComponent("pending_db_\(timestamp).db"))
    }
}

class TestSynchronizerBuilder {
    
    static func build(
        rustBackend: ZcashRustBackendWelding.Type,
        lowerBoundHeight: BlockHeight,
        cacheDbURL: URL,
        dataDbURL: URL,
        pendingDbURL: URL,
        service: LightWalletService,
        repository: TransactionRepository,
        downloader: CompactBlockDownloader,
        spendParamsURL: URL,
        outputParamsURL: URL,
        seedBytes: [UInt8],
        walletBirthday: WalletBirthday,
        loggerProxy: Logger? = nil
    ) throws -> (spendingKeys: [String]?, synchronizer: CombineSynchronizer) {
        
        let initializer = Initializer(
            rustBackend: rustBackend,
            lowerBoundHeight: lowerBoundHeight,
            cacheDbURL: cacheDbURL,
            dataDbURL: dataDbURL,
            pendingDbURL: pendingDbURL,
            service: service,
            repository: repository,
            downloader: downloader,
            spendParamsURL: spendParamsURL,
            outputParamsURL: outputParamsURL,
            loggerProxy: loggerProxy
        )
        let credentials =  try initializer.initialize(seedProvider: StubSeedProvider(bytes: seedBytes), walletBirthdayHeight: walletBirthday.height)
        
        return (credentials, try CombineSynchronizer(initializer: initializer)
        )
    }
}

class StubSeedProvider: SeedProvider {
    
    let bytes: [UInt8]
    init(bytes: [UInt8]) {
        self.bytes = bytes
    }
    func seed() -> [UInt8] {
        self.bytes
    }
}
