//
//  Persistence+MastodonPoll.swift
//
//
//  Created by MainasuK on 2021-12-9.
//

import CoreData
import CoreDataStack
import Foundation
import MastodonSDK

extension Persistence.Poll {

    public struct PersistContext {
        public let domain: String
        public let entity: Mastodon.Entity.Poll
        public let me: MastodonUser?
        public let networkDate: Date
        public init(
            domain: String,
            entity: Mastodon.Entity.Poll,
            me: MastodonUser?,
            networkDate: Date
        ) {
            self.domain = domain
            self.entity = entity
            self.me = me
            self.networkDate = networkDate
        }
    }

    public struct PersistResult {
        public let poll: Poll
        public let isNewInsertion: Bool
        
        public init(
            poll: Poll,
            isNewInsertion: Bool
        ) {
            self.poll = poll
            self.isNewInsertion = isNewInsertion
        }
    }
    
    public static func createOrMerge(
        in managedObjectContext: NSManagedObjectContext,
        context: PersistContext
    ) -> PersistResult {
        
        if let old = fetch(in: managedObjectContext, context: context) {
            merge(in: managedObjectContext, poll: old, context: context)
            return PersistResult(
                poll: old,
                isNewInsertion: false
            )
        } else {
            let poll = create(
                in: managedObjectContext,
                context: context
            )

            return PersistResult(
                poll: poll,
                isNewInsertion: true
            )
        }
    }
    
}

extension Persistence.Poll {
    
    public static func fetch(
        in managedObjectContext: NSManagedObjectContext,
        context: PersistContext
    ) -> Poll? {
        let request = Poll.sortedFetchRequest
        request.predicate = Poll.predicate(domain: context.domain, id: context.entity.id)
        request.fetchLimit = 1
        do {
            return try managedObjectContext.fetch(request).first
        } catch {
            assertionFailure(error.localizedDescription)
            return nil
        }
    }
    
    @discardableResult
    public static func create(
        in managedObjectContext: NSManagedObjectContext,
        context: PersistContext
    ) -> Poll {
        let property = Poll.Property(
            entity: context.entity,
            domain: context.domain,
            networkDate: context.networkDate
        )
        let poll = Poll.insert(
            into: managedObjectContext,
            property: property
        )
        update(in: managedObjectContext, poll: poll, context: context)
        return poll
    }
    
    public static func merge(
        in managedObjectContext: NSManagedObjectContext,
        poll: Poll,
        context: PersistContext
    ) {
        guard context.networkDate > poll.updatedAt else { return }
        let property = Poll.Property(
            entity: context.entity,
            domain: context.domain,
            networkDate: context.networkDate
        )
        poll.update(property: property)
        update(in: managedObjectContext, poll: poll, context: context)
    }
    
    public static func update(
        in managedObjectContext: NSManagedObjectContext,
        poll: Poll,
        context: PersistContext
    ) {
        let optionEntities = context.entity.options
        let options = poll.options.sorted(by: { $0.index < $1.index })
        for (option, entity) in zip(options, optionEntities) {
            Persistence.PollOption.merge(
                option: option,
                context: Persistence.PollOption.PersistContext(
                    index: Int(option.index),
                    poll: poll,
                    entity: entity,
                    me: context.me,
                    networkDate: context.networkDate
                )
            )
        }   // end for in
        
        if let me = context.me {
            if let voted = context.entity.voted {
                poll.update(voted: voted, by: me)
            }
            
            let ownVotes = context.entity.ownVotes ?? []
            for option in options {
                let index = Int(option.index)
                let isVote = ownVotes.contains(index)
                option.update(voted: isVote, by: me)
            }
        }
        
        // update options
        if needsPollOptionsUpdate(context: context, poll: poll) {
            // options differ, update them
            for option in poll.options {
                option.update(poll: nil)
                managedObjectContext.delete(option)
            }
            var attachableOptions = [PollOption]()
            for (index, option) in context.entity.options.enumerated() {
                attachableOptions.append(
                    Persistence.PollOption.create(
                        in: managedObjectContext,
                        context: Persistence.PollOption.PersistContext(
                            index: index,
                            poll: poll,
                            entity: option,
                            me: context.me,
                            networkDate: context.networkDate
                        )
                    )
                )
            }
            poll.attach(options: attachableOptions)
        }
        
        poll.update(updatedAt: context.networkDate)
    }
    
    private static func needsPollOptionsUpdate(context: PersistContext, poll: Poll) -> Bool {
        let entityPollOptions = context.entity.options.map { (title: $0.title, votes: $0.votesCount) }
        let pollOptions = poll.options.sortedByIndex().map { (title: $0.title, votes: Int($0.votesCount)) }
        
        guard entityPollOptions.count == pollOptions.count else {
            // poll definitely needs to be updated due to differences in count of options
            return true
        }
        
        for (entityPollOption, pollOption) in zip(entityPollOptions, pollOptions) {
            guard entityPollOption.title == pollOption.title else {
                // update poll because at least one title differs
                return true
            }
            guard entityPollOption.votes == pollOption.votes else {
                // update poll because at least one vote count differs
                return true
            }
        }
        return false
    }
}
