//
//  StatusPrefetchingService.swift
//  Mastodon
//
//  Created by MainasuK Cirno on 2021-3-10.
//

import os.log
import Foundation
import Combine
import CoreData
import CoreDataStack
import MastodonSDK
import MastodonMeta

final class StatusPrefetchingService {
    
    typealias TaskID = String
    typealias StatusObjectID = NSManagedObjectID
    
    let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPrefetchingService.working-queue")

    // StatusContentOperation
    let statusContentOperationQueue: OperationQueue = {
        let queue = OperationQueue()
        queue.name = "org.joinmastodon.app.StatusPrefetchingService.statusContentOperationQueue"
        queue.maxConcurrentOperationCount = 2
        return queue
    }()
    var statusContentOperations: [StatusObjectID: StatusContentOperation] = [:]

    var disposeBag = Set<AnyCancellable>()
    private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:]

    // input
    weak var apiService: APIService?
    let managedObjectContext: NSManagedObjectContext
    let backgroundManagedObjectContext: NSManagedObjectContext  // read-only
    
    init(
        managedObjectContext: NSManagedObjectContext,
        backgroundManagedObjectContext: NSManagedObjectContext,
        apiService: APIService
    ) {
        self.managedObjectContext = managedObjectContext
        self.backgroundManagedObjectContext = backgroundManagedObjectContext
        self.apiService = apiService
    }

    private func status(from statusObjectItem: StatusObjectItem) -> Status? {
        assert(Thread.isMainThread)
        switch statusObjectItem {
        case .homeTimelineIndex(let objectID):
            let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex
            return homeTimelineIndex?.status
        case .mastodonNotification(let objectID):
            let mastodonNotification = try? managedObjectContext.existingObject(with: objectID) as? MastodonNotification
            return mastodonNotification?.status
        case .status(let objectID):
            let status = try? managedObjectContext.existingObject(with: objectID) as? Status
            return status
        }

    }
    
}

extension StatusPrefetchingService {
    func prefetch(statusObjectItems items: [StatusObjectItem]) {
        for item in items {
            guard let status = status(from: item), !status.isDeleted else { continue }

            // status content parser task
            if statusContentOperations[status.objectID] == nil {
                let mastodonContent = MastodonContent(
                    content: (status.reblog ?? status).content,
                    emojis: (status.reblog ?? status).emojiMeta
                )
                let operation = StatusContentOperation(
                    statusObjectID: status.objectID,
                    mastodonContent: mastodonContent
                )
                statusContentOperations[status.objectID] = operation
                statusContentOperationQueue.addOperation(operation)
            }
        }
    }

    func cancelPrefetch(statusObjectItems items: [StatusObjectItem]) {
        for item in items {
            guard let status = status(from: item), !status.isDeleted else { continue }

            // cancel status content parser task
            statusContentOperations.removeValue(forKey: status.objectID)?.cancel()
        }
    }

}

extension StatusPrefetchingService {
    
    func prefetchReplyTo(
        domain: String,
        statusObjectID: NSManagedObjectID,
        statusID: Mastodon.Entity.Status.ID,
        replyToStatusID: Mastodon.Entity.Status.ID,
        authorizationBox: MastodonAuthenticationBox
    ) {
        workingQueue.async { [weak self] in
            guard let self = self, let apiService = self.apiService else { return }
            let taskID = domain + "@" + statusID + "->" + replyToStatusID
            guard self.statusPrefetchingDisposeBagDict[taskID] == nil else { return }
            os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: prefetching replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID)
            
            self.statusPrefetchingDisposeBagDict[taskID] = apiService.status(
                domain: domain,
                statusID: replyToStatusID,
                authorizationBox: authorizationBox
            )
            .sink(receiveCompletion: { [weak self] completion in
                // remove task when completed
                guard let self = self else { return }
                os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: prefeched replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID)
                self.statusPrefetchingDisposeBagDict[taskID] = nil
            }, receiveValue: { [weak self] _ in
                guard let self = self else { return }
                let backgroundManagedObjectContext = apiService.backgroundManagedObjectContext
                backgroundManagedObjectContext.performChanges {
                    guard let status = backgroundManagedObjectContext.object(with: statusObjectID) as? Status else { return }
                    do {
                        let predicate = Status.predicate(domain: domain, id: replyToStatusID)
                        let request = Status.sortedFetchRequest
                        request.predicate = predicate
                        request.returnsObjectsAsFaults = false
                        request.fetchLimit = 1
                        guard let replyTo = try backgroundManagedObjectContext.fetch(request).first else { return }
                        status.update(replyTo: replyTo)
                    } catch {
                        assertionFailure(error.localizedDescription)
                    }
                }
                .sink { _ in
                    os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update status replyTo: %s", ((#file as NSString).lastPathComponent), #line, #function, taskID)
                } receiveValue: { _ in
                    // do nothing
                }
                .store(in: &self.disposeBag)
            })
        }
    }
    
}
