import Retry
import os.log
import Models
import Shared
import Combine
import Defaults
import XXModels
import XXDatabase
import Foundation
import ToastFeature
import BackupFeature
import NetworkMonitor
import DependencyInjection
import XXLegacyDatabaseMigrator

let logHandler = OSLog(subsystem: "xx.network", category: "Performance debugging")

struct BackupParameters: Codable {
    var email: String?
    var phone: String?
    var username: String
}

struct BackupReport: Codable {
    var contactIds: [String]
    var parameters: String

    private enum CodingKeys: String, CodingKey {
        case parameters = "Params"
        case contactIds = "RestoredContacts"
    }
}

public final class Session: SessionType {
    @KeyObject(.theme, defaultValue: nil) var theme: String?
    @KeyObject(.email, defaultValue: nil) var email: String?
    @KeyObject(.phone, defaultValue: nil) var phone: String?
    @KeyObject(.avatar, defaultValue: nil) var avatar: Data?
    @KeyObject(.username, defaultValue: nil) var username: String?
    @KeyObject(.biometrics, defaultValue: false) var biometrics: Bool
    @KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool
    @KeyObject(.requestCounter, defaultValue: 0) var requestCounter: Int
    @KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool
    @KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool
    @KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool
    @KeyObject(.crashReporting, defaultValue: true) var crashReporting: Bool
    @KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard: Bool
    @KeyObject(.pushNotifications, defaultValue: false) var pushNotifications: Bool
    @KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool

    @Dependency var backupService: BackupService
    @Dependency var toastController: ToastController
    @Dependency var networkMonitor: NetworkMonitoring

    public let client: Client
    public let dbManager: Database
    private var cancellables = Set<AnyCancellable>()

    public var myId: Data { client.bindings.myId }
    public var version: String { type(of: client.bindings).version }

    public var myQR: Data {
        client
            .bindings
            .meMarshalled(
                username!,
                email: isSharingEmail ? email : nil,
                phone: isSharingPhone ? phone : nil
            )
    }

    public var hasRunningTasks: Bool {
        client.bindings.hasRunningTasks
    }

    public var isOnline: AnyPublisher<Bool, Never> {
        networkMonitor.statusPublisher.map { $0 == .available }.eraseToAnyPublisher()
    }

    public init(
        passphrase: String,
        backupFile: Data,
        ndf: String
    ) throws {
        let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking

        os_signpost(.begin, log: logHandler, name: "Decrypting", "Calling newClientFromBackup")
        let (client, backupData) = try network.newClientFromBackup(passphrase: passphrase, data: backupFile, ndf: ndf)
        os_signpost(.end, log: logHandler, name: "Decrypting", "Finished newClientFromBackup")

        self.client = client

        let oldPath = NSSearchPathForDirectoriesInDomains(
            .documentDirectory, .userDomainMask, true
        )[0].appending("/xxmessenger.sqlite")

        let newPath = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
            .appendingPathComponent("database")
            .appendingPathExtension("sqlite").path

        try Migrator.live()(
            try .init(path: oldPath),
            to: try .onDisk(path: newPath),
            myContactId: client.bindings.myId,
            meMarshaled: client.bindings.meMarshalled
        )

        dbManager = try Database.onDisk(path: newPath)

        let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!)

        if !report.parameters.isEmpty {
            let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8))

            username = params.username
            phone = params.phone
            email = params.email
        }

        try continueInitialization()

        if !report.contactIds.isEmpty {
            client.restoreContacts(fromBackup: try! JSONSerialization.data(withJSONObject: report.contactIds))
        }
    }

    public init(ndf: String) throws {
        let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking
        self.client = try network.newClient(ndf: ndf)

        let oldPath = NSSearchPathForDirectoriesInDomains(
            .documentDirectory, .userDomainMask, true
        )[0].appending("/xxmessenger.sqlite")

        let newPath = FileManager.default
            .containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
            .appendingPathComponent("database")
            .appendingPathExtension("sqlite").path

        try Migrator.live()(
            try .init(path: oldPath),
            to: try .onDisk(path: newPath),
            myContactId: client.bindings.myId,
            meMarshaled: client.bindings.meMarshalled
        )

        dbManager = try Database.onDisk(path: newPath)

        try continueInitialization()
    }

    private func continueInitialization() throws {
        setupBindings()
        networkMonitor.start()

        networkMonitor.statusPublisher
            .filter { $0 == .available }.first()
            .sink { [unowned self] _ in client.bindings.replayRequests() }
            .store(in: &cancellables)

        registerUnfinishedTransfers()

        let query = Contact.Query(authStatus: [.verificationInProgress])
        _ = try? dbManager.bulkUpdateContacts(query, .init(authStatus: .verificationFailed))
    }

    public func setDummyTraffic(status: Bool) {
        client.dummyManager?.setStatus(status: status)
    }

    public func deleteMyself() throws {
        guard let username = username, let ud = client.userDiscovery else { return }

        try? unregisterNotifications()
        try ud.deleteMyself(username)

        stop()
        cleanUp()
    }

    private func cleanUp() {
        retry(max: 10, retryStrategy: .delay(seconds: 1)) { [unowned self] in
            guard self.hasRunningTasks == false else { throw NSError.create("") }
        }.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") }

        try? dbManager.drop()
        FileManager.xxCleanup()

        email = nil
        phone = nil
        theme = nil
        avatar = nil
        self.username = nil
        isSharingEmail = false
        isSharingPhone = false
        requestCounter = 0
        biometrics = false
        hideAppList = false
        recordingLogs = true
        crashReporting = true
        icognitoKeyboard = false
        pushNotifications = false
        inappnotifications = true
    }

    private func registerUnfinishedTransfers() {
        guard let unfinishedSendingMessages = try? dbManager.fetchMessages(.init(status: [.sending])),
              let unfinishedSendingTransfers = try? dbManager.fetchFileTransfers(.init(
                id: Set(unfinishedSendingMessages
                    .filter { $0.fileTransferId != nil }
                    .compactMap(\.fileTransferId))))
        else { return }

        // What would be a good way to do this?

        let pairs = unfinishedSendingMessages.map { message -> (Message, FileTransfer) in
            let transfer = unfinishedSendingTransfers.first { ft in
                ft.id == message.fileTransferId
            }

            return (message, transfer!)
        }

        pairs.forEach { message, transfer in
            var message = message
            var transfer = transfer

            do {
                try client.transferManager?.listenUploadFromTransfer(with: transfer.id) { completed, sent, arrived, total, error in
                    if completed {
                        transfer.progress = 1.0
                        message.status = .sent

                    } else {
                        if error != nil {
                            message.status = .sendingFailed
                        } else {
                            transfer.progress = Float(arrived)/Float(total)
                        }
                    }

                    _ = try? self.dbManager.saveFileTransfer(transfer)
                    _ = try? self.dbManager.saveMessage(message)
                }
            } catch {
                message.status = .sendingFailed
                _ = try? self.dbManager.saveMessage(message)
            }
        }
    }

    func updateFactsOnBackup() {
        struct BackupParameters: Codable {
            var email: String?
            var phone: String?
            var username: String

            var jsonFormat: String {
                let data = try! JSONEncoder().encode(self)
                let json = String(data: data, encoding: .utf8)
                return json!
            }
        }

        let params = BackupParameters(
            email: email,
            phone: phone,
            username: username!
        ).jsonFormat

        client.addJson(params)
        backupService.performBackupIfAutomaticIsEnabled()
    }

    private func setupBindings() {
        client.requests
            .sink { [unowned self] in
                if let _ = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
                    return
                }

                if self.inappnotifications {
                    DeviceFeedback.sound(.contactAdded)
                    DeviceFeedback.shake(.notification)
                }

                verify(contact: $0)
            }.store(in: &cancellables)

        client.requestsSent
            .sink { [unowned self] in _ = try? dbManager.saveContact($0) }
            .store(in: &cancellables)

        client.backup
            .throttle(for: .seconds(5), scheduler: DispatchQueue.main, latest: true)
            .sink { [unowned self] in backupService.updateBackup(data: $0) }
            .store(in: &cancellables)

        client.resets
            .sink { [unowned self] in
                /// This will get called when my contact restore its contact.
                /// TODO: Hold a record on the chat that this contact restored.
                ///
                var contact = $0
                contact.authStatus = .friend
                _ = try? dbManager.saveContact(contact)
            }.store(in: &cancellables)

        backupService.settingsPublisher
            .map { $0.enabledService != nil }
            .removeDuplicates()
            .sink { [unowned self] in
                if $0 == true {
                    guard let passphrase = backupService.passphrase else {
                        client.resumeBackup()
                        updateFactsOnBackup()
                        return
                    }

                    client.initializeBackup(passphrase: passphrase)
                    backupService.passphrase = nil
                    updateFactsOnBackup()
                } else {
                    backupService.passphrase = nil
                    client.stopListeningBackup()
                }
            }
            .store(in: &cancellables)

        networkMonitor.statusPublisher
            .sink { print($0) }
            .store(in: &cancellables)

        client.messages
            .sink { [unowned self] in
                if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first {
                    contact.isRecent = false
                    _ = try? dbManager.saveContact(contact)
                }

                _ = try? dbManager.saveMessage($0)
            }.store(in: &cancellables)

        client.network
            .sink { [unowned self] in networkMonitor.update($0) }
            .store(in: &cancellables)

        client.groupRequests
            .sink { [unowned self] request in
                if let _ = try? dbManager.fetchGroups(.init(id: [request.0.id])).first {
                    return
                }

                DispatchQueue.global().async { [weak self] in
                    self?.processGroupCreation(request.0, memberIds: request.1, welcome: request.2)
                }
            }.store(in: &cancellables)

        client.confirmations
            .sink { [unowned self] in
                if var contact = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
                    contact.authStatus = .friend
                    contact.isRecent = true
                    contact.createdAt = Date()
                    _ = try? dbManager.saveContact(contact)

                    toastController.enqueueToast(model: .init(
                        title: contact.nickname ?? contact.username!,
                        subtitle: Localized.Requests.Confirmations.toaster,
                        leftImage: Asset.sharedSuccess.image
                    ))
                }
            }.store(in: &cancellables)
    }
}