import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit

private final class ManagedSynchronizeInstalledStickerPacksOperationsHelper {
    var operationDisposables: [Int32: Disposable] = [:]
    
    func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) {
        var disposeOperations: [Disposable] = []
        var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = []
        
        var hasRunningOperationForPeerId = Set<PeerId>()
        var validMergedIndices = Set<Int32>()
        for entry in entries {
            if !hasRunningOperationForPeerId.contains(entry.peerId) {
                hasRunningOperationForPeerId.insert(entry.peerId)
                validMergedIndices.insert(entry.mergedIndex)
                
                if self.operationDisposables[entry.mergedIndex] == nil {
                    let disposable = MetaDisposable()
                    beginOperations.append((entry, disposable))
                    self.operationDisposables[entry.mergedIndex] = disposable
                }
            }
        }
        
        var removeMergedIndices: [Int32] = []
        for (mergedIndex, disposable) in self.operationDisposables {
            if !validMergedIndices.contains(mergedIndex) {
                removeMergedIndices.append(mergedIndex)
                disposeOperations.append(disposable)
            }
        }
        
        for mergedIndex in removeMergedIndices {
            self.operationDisposables.removeValue(forKey: mergedIndex)
        }
        
        return (disposeOperations, beginOperations)
    }
    
    func reset() -> [Disposable] {
        let disposables = Array(self.operationDisposables.values)
        self.operationDisposables.removeAll()
        return disposables
    }
}

private func withTakenOperation(postbox: Postbox, peerId: PeerId, tag: PeerOperationLogTag, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal<Void, NoError>) -> Signal<Void, NoError> {
    return postbox.transaction { transaction -> Signal<Void, NoError> in
        var result: PeerMergedOperationLogEntry?
        transaction.operationLogUpdateEntry(peerId: peerId, tag: tag, tagLocalIndex: tagLocalIndex, { entry in
            if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizeInstalledStickerPacksOperation  {
                result = entry.mergedEntry!
                return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
            } else {
                return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none)
            }
        })
        
        return f(transaction, result)
        } |> switchToLatest
}

func managedSynchronizeInstalledStickerPacksOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace) -> Signal<Void, NoError> {
    return Signal { _ in
        let tag: PeerOperationLogTag
        switch namespace {
            case .stickers:
                tag = OperationLogTags.SynchronizeInstalledStickerPacks
            case .masks:
                tag = OperationLogTags.SynchronizeInstalledMasks
            case .emoji:
                tag = OperationLogTags.SynchronizeInstalledEmoji
        }
        
        let helper = Atomic<ManagedSynchronizeInstalledStickerPacksOperationsHelper>(value: ManagedSynchronizeInstalledStickerPacksOperationsHelper())
        
        let disposable = postbox.mergedOperationLogView(tag: tag, limit: 10).start(next: { view in
            let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in
                return helper.update(view.entries)
            }
            
            for disposable in disposeOperations {
                disposable.dispose()
            }
            
            for (entry, disposable) in beginOperations {
                let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal<Void, NoError> in
                    if let entry = entry {
                        if let operation = entry.contents as? SynchronizeInstalledStickerPacksOperation {
                            return stateManager.isUpdating
                            |> filter { !$0 }
                            |> take(1)
                            |> mapToSignal { _ -> Signal<Void, NoError> in
                                return postbox.transaction { transaction -> Signal<Void, NoError> in
                                    return synchronizeInstalledStickerPacks(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, namespace: namespace, operation: operation)
                                }
                                |> switchToLatest
                            }
                        } else {
                            assertionFailure()
                        }
                    }
                    return .complete()
                })
                |> then(postbox.transaction { transaction -> Void in
                    let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: tag, tagLocalIndex: entry.tagLocalIndex)
                })
                
                disposable.set((signal |> delay(0.0, queue: Queue.concurrentDefaultQueue())).start())
            }
        })
        
        return ActionDisposable {
            let disposables = helper.with { helper -> [Disposable] in
                return helper.reset()
            }
            for disposable in disposables {
                disposable.dispose()
            }
            disposable.dispose()
        }
    }
}

private func hashForStickerPackInfos(_ infos: [StickerPackCollectionInfo]) -> Int64 {
    var acc: UInt64 = 0
    
    for info in infos {
        combineInt64Hash(&acc, with: UInt64(bitPattern: Int64(info.hash)))
    }
    
    return finalizeInt64Hash(acc)
}

private enum SynchronizeInstalledStickerPacksError {
    case restart
    case done
}

private func fetchStickerPack(network: Network, info: StickerPackCollectionInfo) -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> {
    return network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), hash: 0))
    |> map { result -> (StickerPackCollectionInfo, [ItemCollectionItem]) in
        var items: [ItemCollectionItem] = []
        var updatedInfo = info
        switch result {
        case .stickerSetNotModified:
            break
        case let .stickerSet(stickerSet, packs, keywords, documents):
            updatedInfo = StickerPackCollectionInfo(apiSet: stickerSet, namespace: info.id.namespace)
            var indexKeysByFile: [MediaId: [MemoryBuffer]] = [:]
            for pack in packs {
                switch pack {
                case let .stickerPack(text, fileIds):
                    let key = ValueBoxKey(text).toMemoryBuffer()
                    for fileId in fileIds {
                        let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: fileId)
                        if indexKeysByFile[mediaId] == nil {
                            indexKeysByFile[mediaId] = [key]
                        } else {
                            indexKeysByFile[mediaId]!.append(key)
                        }
                    }
                    break
                }
            }
            for keyword in keywords {
                switch keyword {
                case let .stickerKeyword(documentId, texts):
                    for text in texts {
                        let key = ValueBoxKey(text).toMemoryBuffer()
                        let mediaId = MediaId(namespace: Namespaces.Media.CloudFile, id: documentId)
                        if indexKeysByFile[mediaId] == nil {
                            indexKeysByFile[mediaId] = [key]
                        } else {
                            indexKeysByFile[mediaId]!.append(key)
                        }
                    }
                }
            }
            
            for apiDocument in documents {
                if let file = telegramMediaFileFromApiDocument(apiDocument), let id = file.id {
                    let fileIndexKeys: [MemoryBuffer]
                    if let indexKeys = indexKeysByFile[id] {
                        fileIndexKeys = indexKeys
                    } else {
                        fileIndexKeys = []
                    }
                    items.append(StickerPackItem(index: ItemCollectionItemIndex(index: Int32(items.count), id: id.id), file: file, indexKeys: fileIndexKeys))
                }
            }
            break
        }
        return (updatedInfo, items)
    }
    |> `catch` { _ -> Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError> in
        return .single((info, []))
    }
}

private func resolveStickerPacks(network: Network, remoteInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)], localInfos: [ItemCollectionId: StickerPackCollectionInfo]) -> Signal<[ItemCollectionId: [ItemCollectionItem]], NoError> {
    var signals: [Signal<(ItemCollectionId, [ItemCollectionItem]), NoError>] = []
    for (id, remoteInfoAndIsStale) in remoteInfos {
        let (remoteInfo, isStale) = remoteInfoAndIsStale
        let localInfo = localInfos[id]
        if localInfo == nil || localInfo!.hash != remoteInfo.hash || isStale {
            signals.append(fetchStickerPack(network: network, info: remoteInfo)
            |> map { (info, items) in
                return (info.id, items)
            })
        }
    }
    return combineLatest(signals)
        |> map { result -> [ItemCollectionId: [ItemCollectionItem]] in
            var dict: [ItemCollectionId: [ItemCollectionItem]] = [:]
            for (id, items) in result {
                dict[id] = items
            }
            return dict
        }
}

private func installRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal<Set<ItemCollectionId>, NoError> {
    var signals: [Signal<Set<ItemCollectionId>, NoError>] = []
    for info in infos {
        let install = network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolFalse))
            |> map { result -> Set<ItemCollectionId> in
                switch result {
                    case .stickerSetInstallResultSuccess:
                        return Set()
                    case let .stickerSetInstallResultArchive(archivedSets):
                        var archivedIds = Set<ItemCollectionId>()
                        for archivedSet in archivedSets {
                            switch archivedSet {
                            case let .stickerSetCovered(set, _):
                                archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
                            case let .stickerSetMultiCovered(set, _):
                                archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
                            case let .stickerSetFullCovered(set, _, _, _):
                                archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
                                case let .stickerSetNoCovered(set):
                                    archivedIds.insert(StickerPackCollectionInfo(apiSet: set, namespace: info.id.namespace).id)
                            }
                        }
                        return archivedIds
                }
            }
            |> `catch` { _ -> Signal<Set<ItemCollectionId>, NoError> in
                return .single(Set())
            }
        signals.append(install)
    }
    return combineLatest(signals)
        |> map { idsSets -> Set<ItemCollectionId> in
            var result = Set<ItemCollectionId>()
            for ids in idsSets {
                result.formUnion(ids)
            }
            return result
        }
}

private func removeRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal<Void, NoError> {
    if infos.count > 0 {
        if infos.count > 1 {
            return network.request(Api.functions.messages.toggleStickerSets(flags: 1 << 0, stickersets: infos.map { .inputStickerSetID(id: $0.id.id, accessHash: $0.accessHash) }))
            |> mapToSignal { _ -> Signal<Void, MTRpcError> in
                return .single(Void())
            }
            |> `catch` { _ -> Signal<Void, NoError> in
                return .single(Void())
            }
        } else if let info = infos.first {
            return network.request(Api.functions.messages.uninstallStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash)))
            |> mapToSignal { _ -> Signal<Void, MTRpcError> in
                return .single(Void())
            }
            |> `catch` { _ -> Signal<Void, NoError> in
                return .single(Void())
            }
        }
    }
    return .single(Void())
}

private func archiveRemoteStickerPacks(network: Network, infos: [StickerPackCollectionInfo]) -> Signal<Void, NoError> {
    if infos.count > 0 {
        if infos.count > 1 {
            return network.request(Api.functions.messages.toggleStickerSets(flags: 1 << 1, stickersets: infos.map { .inputStickerSetID(id: $0.id.id, accessHash: $0.accessHash) }))
            |> mapToSignal { _ -> Signal<Void, MTRpcError> in
                return .single(Void())
            }
            |> `catch` { _ -> Signal<Void, NoError> in
                return .single(Void())
            }
        } else if let info = infos.first {
            return network.request(Api.functions.messages.installStickerSet(stickerset: .inputStickerSetID(id: info.id.id, accessHash: info.accessHash), archived: .boolTrue))
            |> mapToSignal { _ -> Signal<Void, MTRpcError> in
                return .single(Void())
            }
            |> `catch` { _ -> Signal<Void, NoError> in
                return .single(Void())
            }
        }
    }
    return .single(Void())
}

private func reorderRemoteStickerPacks(network: Network, namespace: SynchronizeInstalledStickerPacksOperationNamespace, ids: [ItemCollectionId]) -> Signal<Void, NoError> {
    var flags: Int32 = 0
    switch namespace {
        case .stickers:
            break
        case .masks:
            flags |= (1 << 0)
        case .emoji:
            flags |= (1 << 1)
    }
    return network.request(Api.functions.messages.reorderStickerSets(flags: flags, order: ids.map { $0.id }))
        |> `catch` { _ -> Signal<Api.Bool, NoError> in
            return .single(.boolFalse)
        }
        |> mapToSignal { _ -> Signal<Void, NoError> in
            return .complete()
        }
}

private func synchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal<Void, NoError> {
    let collectionNamespace: ItemCollectionId.Namespace
    switch namespace {
        case .stickers:
            collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
        case .masks:
            collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
        case .emoji:
            collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
    }
    
    let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
    let localStateCollectionOrder = localCollectionInfos.map({ $0.id })
    let localPreviousStateCollectionOrder = operation.previousPacks
    
    if localStateCollectionOrder == localPreviousStateCollectionOrder {
        return continueSynchronizeInstalledStickerPacks(
            transaction: transaction,
            postbox: postbox,
            network: network,
            stateManager: stateManager,
            namespace: namespace,
            operation: operation
        )
    }
    
    let locallyAddedInfos = localCollectionInfos.filter { !localPreviousStateCollectionOrder.contains($0.id) }
    
    if locallyAddedInfos.isEmpty {
        return continueSynchronizeInstalledStickerPacks(
            transaction: transaction,
            postbox: postbox,
            network: network,
            stateManager: stateManager,
            namespace: namespace,
            operation: operation
        )
    }
    
    var fetchSignals: [Signal<(StickerPackCollectionInfo, [ItemCollectionItem]), NoError>] = []
    for info in locallyAddedInfos {
        fetchSignals.append(fetchStickerPack(network: network, info: info))
    }
    let fetchedPacks = combineLatest(fetchSignals)
    
    return fetchedPacks
    |> mapToSignal { fetchedPacks -> Signal<Void, NoError> in
        return postbox.transaction { transaction -> Signal<Void, NoError> in
            var currentInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
            for (info, items) in fetchedPacks {
                if let index = currentInfos.firstIndex(where: { $0.id == info.id }) {
                    currentInfos[index] = info
                    transaction.replaceItemCollectionItems(collectionId: info.id, items: items)
                }
            }
            transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: currentInfos.map { ($0.id, $0) })
            
            return continueSynchronizeInstalledStickerPacks(
                transaction: transaction,
                postbox: postbox,
                network: network,
                stateManager: stateManager,
                namespace: namespace,
                operation: operation
            )
        }
        |> switchToLatest
    }
}

/*#if DEBUG

func debugFetchAllStickers(account: Account) -> Signal<Never, NoError> {
    let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers]
    let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
    let stickerItems: Signal<[TelegramMediaFile], NoError> = account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000)
    |> map { view -> [TelegramMediaFile] in
        var files: [TelegramMediaFile] = []
        for entry in view.entries {
            guard let item = entry.item as? StickerPackItem else {
                continue
            }
            if !item.file.isAnimatedSticker {
                continue
            }
            files.append(item.file)
        }
        return files
    }
    |> take(1)
    
    return stickerItems
    |> mapToSignal { files -> Signal<Never, NoError> in
        var loadFileSignals: [Signal<Never, NoError>] = []
        
        let tempDir = TempBox.shared.tempDirectory()
        print("debugFetchAllStickers into \(tempDir.path)")
        
        for file in files {
            loadFileSignals.append(Signal { subscriber in
                let fetch = fetchedMediaResource(mediaBox: account.postbox.mediaBox, reference: stickerPackFileReference(file).resourceReference(file.resource)).start()
                let data = (account.postbox.mediaBox.resourceData(file.resource)
                |> filter { $0.complete }
                |> take(1)).start(next: { data in
                    if let dataValue = try? Data(contentsOf: URL(fileURLWithPath: data.path)), let unpackedData = TGGUnzipData(dataValue, 5 * 1024 * 1024) {
                        let filePath = tempDir.path + "/\(file.fileId.id).json"
                        let _ = try? unpackedData.write(to: URL(fileURLWithPath: filePath), options: .atomic)
                        subscriber.putCompletion()
                    }
                })
                
                return ActionDisposable {
                    fetch.dispose()
                    data.dispose()
                }
            })
        }
        
        return combineLatest(loadFileSignals)
        |> ignoreValues
    }
}

#endif*/

private func continueSynchronizeInstalledStickerPacks(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, namespace: SynchronizeInstalledStickerPacksOperationNamespace, operation: SynchronizeInstalledStickerPacksOperation) -> Signal<Void, NoError> {
    let collectionNamespace: ItemCollectionId.Namespace
    switch namespace {
        case .stickers:
            collectionNamespace = Namespaces.ItemCollection.CloudStickerPacks
        case .masks:
            collectionNamespace = Namespaces.ItemCollection.CloudMaskPacks
        case .emoji:
            collectionNamespace = Namespaces.ItemCollection.CloudEmojiPacks
    }
    
    let localCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
    let initialLocalHash = hashForStickerPackInfos(localCollectionInfos)
    
    let request: Signal<Api.messages.AllStickers, MTRpcError>
    switch namespace {
        case .stickers:
            request = network.request(Api.functions.messages.getAllStickers(hash: initialLocalHash))
        case .masks:
            request = network.request(Api.functions.messages.getMaskStickers(hash: initialLocalHash))
        case .emoji:
            request = network.request(Api.functions.messages.getEmojiStickers(hash: initialLocalHash))
    }
    
    let sequence = request
    |> retryRequest
    |> mapError { _ -> SynchronizeInstalledStickerPacksError in
    }
    |> mapToSignal { result -> Signal<Void, SynchronizeInstalledStickerPacksError> in
        return postbox.transaction { transaction -> Signal<Void, SynchronizeInstalledStickerPacksError> in
            let checkLocalCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
            if checkLocalCollectionInfos != localCollectionInfos {
                return .fail(.restart)
            }
            
            let localInitialStateCollectionOrder = operation.previousPacks
            let localInitialStateCollectionIds = Set(localInitialStateCollectionOrder)
            
            let localCollectionOrder = checkLocalCollectionInfos.map { $0.id }
            let localCollectionIds = Set(localCollectionOrder)
            
            var remoteCollectionInfos: [StickerPackCollectionInfo] = []
            switch result {
                case let .allStickers(_, sets):
                    for apiSet in sets {
                        let info = StickerPackCollectionInfo(apiSet: apiSet, namespace: collectionNamespace)
                        remoteCollectionInfos.append(info)
                    }
                case .allStickersNotModified:
                    remoteCollectionInfos = checkLocalCollectionInfos
            }
            
            let remoteCollectionOrder = remoteCollectionInfos.map { $0.id }
            let remoteCollectionIds = Set(remoteCollectionOrder)
            
            var remoteInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
            for info in remoteCollectionInfos {
                remoteInfos[info.id] = info
            }
            var localInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:]
            for info in checkLocalCollectionInfos {
                localInfos[info.id] = info
            }
            
            if localInitialStateCollectionOrder == localCollectionOrder {
                if checkLocalCollectionInfos == remoteCollectionInfos {
                    return .fail(.done)
                } else {
                    var resolveRemoteInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)] = [:]
                    for (id, info) in remoteInfos {
                        resolveRemoteInfos[id] = (info, false)
                    }
                    return resolveStickerPacks(network: network, remoteInfos: resolveRemoteInfos, localInfos: localInfos)
                    |> mapError { _ -> SynchronizeInstalledStickerPacksError in
                    }
                    |> mapToSignal { replaceItems -> Signal<Void, SynchronizeInstalledStickerPacksError> in
                        return postbox.transaction { transaction -> Signal<Void, SynchronizeInstalledStickerPacksError> in
                            let storeSignal: Signal<Void, NoError>
                            if localCollectionIds.isEmpty {
                                var incrementalSignal = postbox.transaction { transaction -> Void in
                                    transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: remoteCollectionInfos.map { ($0.id, $0) })
                                    for id in localCollectionIds.subtracting(remoteCollectionIds) {
                                        transaction.replaceItemCollectionItems(collectionId: id, items: [])
                                    }
                                }
                                for (id, items) in replaceItems {
                                    let partSignal = postbox.transaction { transaction -> Void in
                                        transaction.replaceItemCollectionItems(collectionId: id, items: items)
                                    }
                                    incrementalSignal = incrementalSignal
                                    |> then(
                                        partSignal
                                        |> delay(0.01, queue: Queue.concurrentDefaultQueue())
                                    )
                                }
                                storeSignal = incrementalSignal
                            } else {
                                storeSignal = postbox.transaction { transaction -> Void in
                                    transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: remoteCollectionInfos.map { ($0.id, $0) })
                                    for (id, items) in replaceItems {
                                        transaction.replaceItemCollectionItems(collectionId: id, items: items)
                                    }
                                    for id in localCollectionIds.subtracting(remoteCollectionIds) {
                                        transaction.replaceItemCollectionItems(collectionId: id, items: [])
                                    }
                                }
                            }
                            

                            let storePremiumSignal: Signal<Void, SynchronizeInstalledStickerPacksError> = postbox.transaction { transaction -> Void in
                                var premiumStickers: [OrderedItemListEntry] = []
                                for (id, _) in remoteInfos {
                                    let items = transaction.getItemCollectionItems(collectionId: id)
                                    for item in items {
                                        if let stickerItem = item as? StickerPackItem, stickerItem.file.isPremiumSticker,
                                           let entry = CodableEntry(RecentMediaItem(stickerItem.file)) {
                                            premiumStickers.append(OrderedItemListEntry(id: RecentMediaItemId(stickerItem.file.fileId).rawValue, contents: entry))
                                        }
                                    }
                                }
                                transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.PremiumStickers, items: premiumStickers)
                            } |> castError(SynchronizeInstalledStickerPacksError.self)
                            
                            return (
                                storeSignal
                                |> mapError { _ -> SynchronizeInstalledStickerPacksError in }
                            )
                            |> then(storePremiumSignal)
                            |> then(.fail(.done))
                        }
                        |> castError(SynchronizeInstalledStickerPacksError.self)
                        |> switchToLatest
                    }
                }
            } else {
                let locallyRemovedCollectionIds = localInitialStateCollectionIds.subtracting(localCollectionIds)
                let locallyAddedCollectionIds = localCollectionIds.subtracting(localInitialStateCollectionIds)
                let remotelyAddedCollections = remoteCollectionInfos.filter { info in
                    return !locallyRemovedCollectionIds.contains(info.id) && !localCollectionIds.contains(info.id)
                }
                let remotelyRemovedCollectionIds = remoteCollectionIds.subtracting(localInitialStateCollectionIds).subtracting(locallyAddedCollectionIds)
                
                var resultingCollectionInfos: [(StickerPackCollectionInfo, Bool)] = []
                resultingCollectionInfos.append(contentsOf: remotelyAddedCollections.map { ($0, false) })
                var remoteCollectionInfoMap: [ItemCollectionId: StickerPackCollectionInfo] = [:]
                for info in remoteCollectionInfos {
                    remoteCollectionInfoMap[info.id] = info
                }
                for info in checkLocalCollectionInfos {
                    if !remotelyRemovedCollectionIds.contains(info.id) {
                        if let remoteInfo = remoteCollectionInfoMap[info.id] {
                            resultingCollectionInfos.append((remoteInfo, false))
                        } else {
                            resultingCollectionInfos.append((info, true))
                        }
                    }
                }
                
                let resultingCollectionIds = Set(resultingCollectionInfos.map { $0.0.id })
                let removeRemoteCollectionIds = remoteCollectionIds.subtracting(resultingCollectionIds)
                let addRemoteCollectionIds = resultingCollectionIds.subtracting(remoteCollectionIds)
                
                var removeRemoteCollectionInfos: [StickerPackCollectionInfo] = []
                for id in removeRemoteCollectionIds {
                    if let info = remoteInfos[id] {
                        removeRemoteCollectionInfos.append(info)
                    } else if let info = localInfos[id] {
                        removeRemoteCollectionInfos.append(info)
                    }
                }
                
                var addRemoteCollectionInfos: [StickerPackCollectionInfo] = []
                for id in addRemoteCollectionIds {
                    if let info = remoteInfos[id] {
                        addRemoteCollectionInfos.append(info)
                    } else if let info = localInfos[id] {
                        addRemoteCollectionInfos.append(info)
                    }
                }
                
                let infosToArchive = removeRemoteCollectionInfos.filter { info in
                    return operation.archivedPacks.contains(info.id)
                }
                let infosToRemove = removeRemoteCollectionInfos.filter { info in
                    return !operation.archivedPacks.contains(info.id)
                }
                
                let archivedOrRemovedIds = (combineLatest(archiveRemoteStickerPacks(network: network, infos: infosToArchive), removeRemoteStickerPacks(network: network, infos: infosToRemove))
                |> mapToSignal { _ -> Signal<Void, NoError> in
                    return .complete()
                }
                |> then(Signal<Void, NoError>.single(Void())))
                |> mapToSignal { _ -> Signal<Set<ItemCollectionId>, NoError> in
                    return installRemoteStickerPacks(network: network, infos: addRemoteCollectionInfos)
                    |> mapToSignal { ids -> Signal<Set<ItemCollectionId>, NoError> in
                        return (reorderRemoteStickerPacks(network: network, namespace: namespace, ids: resultingCollectionInfos.map({ $0.0.id }).filter({ !ids.contains($0) }))
                        |> then(Signal<Void, NoError>.single(Void())))
                        |> map { _ -> Set<ItemCollectionId> in
                            return ids
                        }
                    }
                }
                
                var resultingInfos: [ItemCollectionId: (StickerPackCollectionInfo, Bool)] = [:]
                for info in resultingCollectionInfos {
                    resultingInfos[info.0.id] = info
                }
                
                let resolvedItems = resolveStickerPacks(network: network, remoteInfos: resultingInfos, localInfos: localInfos)
                
                return combineLatest(archivedOrRemovedIds, resolvedItems)
                |> mapError { _ -> SynchronizeInstalledStickerPacksError in }
                |> mapToSignal { archivedOrRemovedIds, replaceItems -> Signal<Void, SynchronizeInstalledStickerPacksError> in
                    return (postbox.transaction { transaction -> Signal<Void, SynchronizeInstalledStickerPacksError> in
                        let finalCheckLocalCollectionInfos = transaction.getItemCollectionsInfos(namespace: collectionNamespace).map { $0.1 as! StickerPackCollectionInfo }
                        if finalCheckLocalCollectionInfos != localCollectionInfos {
                            return .fail(.restart)
                        }
                        
                        transaction.replaceItemCollectionInfos(namespace: collectionNamespace, itemCollectionInfos: resultingCollectionInfos.filter({ info in
                            return !archivedOrRemovedIds.contains(info.0.id)
                        }).map({ ($0.0.id, $0.0) }))
                        for (id, items) in replaceItems {
                            if !archivedOrRemovedIds.contains(id) {
                                transaction.replaceItemCollectionItems(collectionId: id, items: items)
                            }
                        }
                        for id in localCollectionIds.subtracting(resultingCollectionIds).union(archivedOrRemovedIds) {
                            transaction.replaceItemCollectionItems(collectionId: id, items: [])
                        }
                        
                        var premiumStickers: [OrderedItemListEntry] = []
                        for id in resultingCollectionIds {
                            let items = transaction.getItemCollectionItems(collectionId: id)
                            for item in items {
                                if let stickerItem = item as? StickerPackItem, stickerItem.file.isPremiumSticker,
                                   let entry = CodableEntry(RecentMediaItem(stickerItem.file)) {
                                    premiumStickers.append(OrderedItemListEntry(id: RecentMediaItemId(stickerItem.file.fileId).rawValue, contents: entry))
                                }
                            }
                        }
                        transaction.replaceOrderedItemListItems(collectionId: Namespaces.OrderedItemList.PremiumStickers, items: premiumStickers)
                        
                        return .complete()
                    }
                    |> mapError { _ -> SynchronizeInstalledStickerPacksError in
                    })
                    |> switchToLatest
                    |> then(.fail(.done))
                }
            }
        }
        |> mapError { _ -> SynchronizeInstalledStickerPacksError in
        }
        |> switchToLatest
    }
    
    return ((sequence
    |> `catch` { error -> Signal<Void, SynchronizeInstalledStickerPacksError> in
        switch error {
            case .done:
                return .fail(.done)
            case .restart:
                return .complete()
        }
    }) |> restart) |> `catch` { _ -> Signal<Void, NoError> in
        return .complete()
    }
}