import HUD
import Shared
import Models
import Combine
import Defaults
import Foundation
import Integration
import DependencyInjection

protocol ChatListViewModelType {
    var myId: Data { get }
    var username: String { get }
    var editState: EditStateHandler { get }
    var searchQueryRelay: CurrentValueSubject<String, Never> { get }
    var chatsRelay: CurrentValueSubject<[GenericChatInfo], Never> { get }

    var isOnline: AnyPublisher<Bool, Never> { get }
    var badgeCount: AnyPublisher<Int, Never> { get }

    func delete(indexPaths: [IndexPath]?)
}

final class ChatListViewModel: ChatListViewModelType {
    @Dependency private var session: SessionType

    @KeyObject(.username, defaultValue: "") var myUsername: String

    let editState = EditStateHandler()
    let chatsRelay = CurrentValueSubject<[GenericChatInfo], Never>([])
    let searchQueryRelay = CurrentValueSubject<String, Never>("")
    private var cancellables = Set<AnyCancellable>()

    var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() }
    private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none)

    var badgeCount: AnyPublisher<Int, Never> {
        Publishers.CombineLatest(
            session.contacts(.received),
            session.groups(.pending)
        ).map { $0.0.count + $0.1.count }
        .eraseToAnyPublisher()
    }

    var isOnline: AnyPublisher<Bool, Never> { session.isOnline }

    var myId: Data { session.myId }

    var username: String { myUsername }

    init() {
        let searchStream = searchQueryRelay
            .removeDuplicates()
            .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
            .eraseToAnyPublisher()

        Publishers.CombineLatest3(
            session.singleChats(.all),
            session.groupChats(.accepted),
            searchStream
        ).map { data -> [GenericChatInfo] in
            let singles = data.0
            let groupies = data.1
            let searched = data.2

            var generics = [GenericChatInfo]()

            for single in singles {
                generics.append(
                    GenericChatInfo(
                        contact: single.contact,
                        groupInfo: nil,
                        latestE2EMessage: single.lastMessage
                    )
                )
            }

            for group in groupies {
                generics.append(
                    GenericChatInfo(
                        contact: nil,
                        groupInfo: group,
                        latestE2EMessage: nil
                    )
                )
            }

            if !searched.isEmpty {
                generics = generics.filter { filtering in
                    if let contact = filtering.contact {
                        let username = contact.username.lowercased().contains(searched.lowercased())
                        let nickname = contact.nickname?.lowercased().contains(searched.lowercased()) ?? false
                        let lastMessage = filtering.latestE2EMessage?.payload.text.lowercased().contains(searched.lowercased()) ?? false

                        return username || nickname || lastMessage
                    } else {
                        if let group = filtering.groupInfo?.group {
                            let name = group.name.lowercased().contains(searched.lowercased())
                            let last = filtering.groupInfo?.lastMessage?.payload.text.lowercased().contains(searched.lowercased()) ?? false
                            return name || last
                        }
                    }

                    return false
                }
            }

            #warning("TODO: Use enum to differentiate chats")

            return generics.sorted { infoA, infoB in
                if let singleA = infoA.latestE2EMessage {
                    if let singleB = infoB.latestE2EMessage {
                        /// aSingle bSingle
                        return singleA.timestamp > singleB.timestamp
                    } else {
                        /// aSingle bGroup
                        let groupB = infoB.groupInfo!

                        if let lastGM = groupB.lastMessage {
                            /// aSingle bGroup w/ message
                            return singleA.timestamp > lastGM.timestamp
                        } else {
                            /// aSingle bGroup w/out message
                            return true
                        }
                    }
                } else {
                    let groupA = infoA.groupInfo!

                    if let lastGM = groupA.lastMessage {
                        /// aGroup w/ message

                        if let singleB = infoB.latestE2EMessage {
                            /// aGroup w/ message bSingle

                            return lastGM.timestamp > singleB.timestamp
                        } else {
                            let groupB = infoB.groupInfo!
                            /// aGroup w/ message bGroup

                            if let lastGM2 = groupB.lastMessage {
                                return lastGM.timestamp > lastGM2.timestamp
                            } else {
                                return true
                            }
                        }
                    } else {
                        /// aGroup w/out message b?
                        return false
                    }
                }
            }
        }.sink { [unowned self] in chatsRelay.send($0)  }
        .store(in: &cancellables)
    }

    func isGroup(indexPath: IndexPath) -> Bool {
        chatsRelay.value[indexPath.row].contact == nil
    }

    func deleteAndLeaveGroupFrom(indexPath: IndexPath) {
        guard let group = chatsRelay.value[indexPath.row].groupInfo?.group else {
            fatalError("Tried to delete a group from an index path that is not one")
        }

        do {
            hudRelay.send(.on(nil))
            try session.leave(group: group)
            hudRelay.send(.none)
        } catch {
            hudRelay.send(.error(.init(with: error)))
        }
    }

    func delete(indexPaths: [IndexPath]?) {
        guard let selectedIndexPaths = indexPaths else {
            let contacts = chatsRelay.value.compactMap { $0.contact }
            let groups = chatsRelay.value.compactMap { $0.groupInfo?.group }

            groups.forEach(session.deleteAll(from:))
            contacts.forEach(session.deleteAll(from:))
            return
        }

        let contacts = selectedIndexPaths.compactMap { chatsRelay.value[$0.row].contact }
        let groups = selectedIndexPaths.compactMap { chatsRelay.value[$0.row].groupInfo?.group }

        groups.forEach(session.deleteAll(from:))
        contacts.forEach(session.deleteAll(from:))
    }
}