import HUD
import UIKit
import Models
import Shared
import Combine
import Defaults
import XXModels
import Integration
import DrawerFeature
import CombineSchedulers
import DependencyInjection

struct RequestReceived: Hashable, Equatable {
    var request: Request?
    var isHidden: Bool
    var leader: String?
}

final class RequestsReceivedViewModel {
    @Dependency private var session: SessionType

    @KeyObject(.isShowingHiddenRequests, defaultValue: false) var isShowingHiddenRequests: Bool

    var hudPublisher: AnyPublisher<HUDStatus, Never> {
        hudSubject.eraseToAnyPublisher()
    }

    var verifyingPublisher: AnyPublisher<Void, Never> {
        verifyingSubject.eraseToAnyPublisher()
    }

    var itemsPublisher: AnyPublisher<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never> {
        itemsSubject.eraseToAnyPublisher()
    }

    var groupConfirmationPublisher: AnyPublisher<Group, Never> {
        groupConfirmationSubject.eraseToAnyPublisher()
    }

    var contactConfirmationPublisher: AnyPublisher<Contact, Never> {
        contactConfirmationSubject.eraseToAnyPublisher()
    }

    private var cancellables = Set<AnyCancellable>()
    private let updateSubject = CurrentValueSubject<Void, Never>(())
    private let verifyingSubject = PassthroughSubject<Void, Never>()
    private let hudSubject = CurrentValueSubject<HUDStatus, Never>(.none)
    private let groupConfirmationSubject = PassthroughSubject<Group, Never>()
    private let contactConfirmationSubject = PassthroughSubject<Contact, Never>()
    private let itemsSubject = CurrentValueSubject<NSDiffableDataSourceSnapshot<Section, RequestReceived>, Never>(.init())

    var backgroundScheduler: AnySchedulerOf<DispatchQueue> = DispatchQueue.global().eraseToAnyScheduler()

    init() {
        let groupsQuery = Group.Query(
            authStatus: [
                .hidden,
                .pending
            ])

        let contactsQuery = Contact.Query(
            authStatus: [
                .friend,
                .hidden,
                .verified,
                .verificationFailed,
                .verificationInProgress
            ], isBlocked: false, isBanned: false)

        let groupStream = session.dbManager.fetchGroupsPublisher(groupsQuery).assertNoFailure()
        let contactsStream = session.dbManager.fetchContactsPublisher(contactsQuery).assertNoFailure()

        Publishers.CombineLatest3(
            groupStream,
            contactsStream,
            updateSubject.eraseToAnyPublisher()
        )
        .subscribe(on: DispatchQueue.main)
        .receive(on: DispatchQueue.global())
        .map { [unowned self] data -> NSDiffableDataSourceSnapshot<Section, RequestReceived> in
            var snapshot = NSDiffableDataSourceSnapshot<Section, RequestReceived>()
            snapshot.appendSections([.appearing, .hidden])

            let contactsFilteringFriends = data.1.filter { $0.authStatus != .friend }
            let requests = data.0.map(Request.group) + contactsFilteringFriends.map(Request.contact)
            let receivedRequests = requests.map { request -> RequestReceived in
                switch request {
                case let .group(group):
                    func leaderName() -> String {
                        if let leader = data.1.first(where: { $0.id == group.leaderId }) {
                            return (leader.nickname ?? leader.username) ?? "Leader is not a friend"
                        } else {
                            return "[Error retrieving leader]"
                        }
                    }

                    return RequestReceived(
                        request: request,
                        isHidden: group.authStatus == .hidden,
                        leader: leaderName()
                    )
                case let .contact(contact):
                    return RequestReceived(
                        request: request,
                        isHidden: contact.authStatus == .hidden,
                        leader: nil
                    )
                }
            }

            if self.isShowingHiddenRequests {
                snapshot.appendItems(receivedRequests.filter(\.isHidden), toSection: .hidden)
            }

            guard receivedRequests.filter({ $0.isHidden == false }).count > 0 else {
                snapshot.appendItems([RequestReceived(isHidden: false)], toSection: .appearing)
                return snapshot
            }

            snapshot.appendItems(receivedRequests.filter { $0.isHidden == false }, toSection: .appearing)
            return snapshot
        }.sink(
            receiveCompletion: { _ in },
            receiveValue: { [unowned self] in itemsSubject.send($0) }
        ).store(in: &cancellables)
    }

    func didToggleHiddenRequestsSwitcher() {
        isShowingHiddenRequests.toggle()
        updateSubject.send()
    }

    func didTapStateButtonFor(request: Request) {
        guard case let .contact(contact) = request else { return }

        if request.status == .failedToVerify {
            backgroundScheduler.schedule { [weak self] in
                self?.session.verify(contact: contact)
            }
        } else if request.status == .verifying {
            verifyingSubject.send()
        }
    }

    func didRequestHide(group: Group) {
        if var group = try? session.dbManager.fetchGroups(.init(id: [group.id])).first {
            group.authStatus = .hidden
            _ = try? session.dbManager.saveGroup(group)
        }
    }

    func didRequestAccept(group: Group) {
        hudSubject.send(.on)

        backgroundScheduler.schedule { [weak self] in
            do {
                try self?.session.join(group: group)
                self?.hudSubject.send(.none)
                self?.groupConfirmationSubject.send(group)
            } catch {
                self?.hudSubject.send(.error(.init(with: error)))
            }
        }
    }

    func fetchMembers(
        _ group: Group,
        _ completion: @escaping (Result<[DrawerTableCellModel], Error>) -> Void
    ) {
        if let info = try? session.dbManager.fetchGroupInfos(.init(groupId: group.id)).first {
            session.dbManager.fetchContactsPublisher(.init(id: Set(info.members.map(\.id))))
                .assertNoFailure()
                .sink { members in
                    let withUsername = members
                        .filter { $0.username != nil }
                        .map {
                            DrawerTableCellModel(
                                id: $0.id,
                                title: $0.nickname ?? $0.username!,
                                image: $0.photo,
                                isCreator: $0.id == group.leaderId,
                                isConnection: $0.authStatus == .friend
                            )
                        }

                    let withoutUsername = members
                        .filter { $0.username == nil }
                        .map {
                            DrawerTableCellModel(
                                id: $0.id,
                                title: "Fetching username...",
                                image: $0.photo,
                                isCreator: $0.id == group.leaderId,
                                isConnection: $0.authStatus == .friend
                            )
                        }

                    completion(.success(withUsername + withoutUsername))
                }.store(in: &cancellables)
        }
    }

    func didRequestHide(contact: Contact) {
        if var contact = try? session.dbManager.fetchContacts(.init(id: [contact.id])).first {
            contact.authStatus = .hidden
            _ = try? session.dbManager.saveContact(contact)
        }
    }

    func didRequestAccept(contact: Contact, nickname: String? = nil) {
        hudSubject.send(.on)

        var contact = contact
        contact.nickname = nickname ?? contact.username

        backgroundScheduler.schedule { [weak self] in
            do {
                try self?.session.confirm(contact)
                self?.hudSubject.send(.none)
                self?.contactConfirmationSubject.send(contact)
            } catch {
                self?.hudSubject.send(.error(.init(with: error)))
            }
        }
    }

    func groupChatWith(group: Group) -> GroupInfo {
        guard let info = try? session.dbManager.fetchGroupInfos(.init(groupId: group.id)).first else {
            fatalError()
        }

        return info
    }
}