import APIClient
import Foundation

/*
    This will be used in the cache dictionary in comment client
    to store comments as a result of requests.

    These will then be used in various ways to display comments
    when the channel has updated.
 */
public struct ClipCommentsThread: Equatable {
    public var clipRemoteID: String // ClipID.remoteId
    public var nextPageCursor: String?
    public var allowComment: Bool
    public var disableReason: String?

    /* The reply cursor for each comment */
    public var replyNextPageCursor: [String: String] = [:]

    /* Value Source */
    private var clipComments: Set<ClipComment> = []

    // Processed Values
    /* A map of all the comments in the clip */
    private var commentsMap: [String: ClipComment] = [:]
    private var unparentedCommentsInSortedOrder: [String] = []
    private var replyMapInSortedOrder: [String: [String]] = [:]
    public private(set) var hasMoreRepliesMap: [String: Bool] = [:]

    public func commentById(_ commentID: String) -> ClipComment? {
        return commentsMap[commentID]
    }

    public var allSortedUnparentedComments: [ClipComment] {
        return unparentedCommentsInSortedOrder.compactMap { commentID in commentsMap[commentID] }
    }

    public var allSortedRepliesForComment: [String: [ClipComment]] {
        var allRepliesMap: [String: [ClipComment]] = [:]
        for parentCommentID in unparentedCommentsInSortedOrder {
            let replyCommentIDs: [String] = replyMapInSortedOrder[parentCommentID] ?? []
            let mappedReplies = replyCommentIDs.compactMap { commentID in commentsMap[commentID] }
            allRepliesMap[parentCommentID] = mappedReplies
        }
        return allRepliesMap
    }

    public func sortedRepliesForComment(_ parentCommentID: String) -> [ClipComment] {
        let replyCommentIDs: [String] = replyMapInSortedOrder[parentCommentID] ?? []
        return replyCommentIDs.compactMap { commentID in commentsMap[commentID] }
    }

    mutating func addPage(_ newPage: CommentsPage) {
        nextPageCursor = newPage.nextCursor
        allowComment = newPage.allowComment
        disableReason = newPage.disableReason
        clipComments.formUnion(newPage.results)
        for newComment in newPage.results {
            if let replyContinuationToken = newComment.replyContinuationToken {
                replyNextPageCursor[newComment.id] = replyContinuationToken
            }
        }
        prepItem()
    }

    mutating func addComment(_ newComment: ClipComment) {
        clipComments.insert(newComment)
        if let replyContinuationToken = newComment.replyContinuationToken {
            replyNextPageCursor[newComment.id] = replyContinuationToken
        }
        prepItem()
    }

    mutating func addRepliesPage(_ newPage: CommentRepliesPage, rootCommentID: String) {
        if let cursor = newPage.replyContinuationToken {
            replyNextPageCursor[rootCommentID] = cursor
        } else {
            replyNextPageCursor.removeValue(forKey: rootCommentID)
        }

        clipComments.formUnion(newPage.replies)
        prepItem()
    }

    public func replyCursorForComment(_ commentID: String) -> String? {
        return replyNextPageCursor[commentID]
    }

    public func hasNextPageToLoad(_ commentID: String) -> Bool {
        let replyIDs = replyMapInSortedOrder[commentID] ?? []
        let replyCursorForComment = replyCursorForComment(commentID)
        let replyCount = replyIDs.count
        return !(replyCount == 0 || replyCursorForComment == nil)
    }

    mutating func updateCommentReaciton(
        _ commentID: String,
        _ reaction: CommentReactionType,
        _ numLikes: Int
    ) {
        guard let originalComment = commentById(commentID) else { return }
        var mutableCopy = originalComment
        mutableCopy.numLikes = numLikes
        mutableCopy.reactionType = reaction.propertyValue
        clipComments.remove(originalComment)
        clipComments.insert(mutableCopy)
        commentsMap[commentID] = mutableCopy
    }

    mutating func deleteComment(_ commentID: String) {
        /* This assumes you will be deleting all replies as well as the parent comment */
        clipComments = clipComments.filter { !($0.id == commentID || $0.parentID == commentID) }

        /*
            Cache sometimes carries a nested copy of a comment
            in .replies on a clip comment and potentially a duplicated
            in paged replies response

            Remove the one that is nested inside the replies of a comment
            as well so it doesn't get readded, by existing in two places.

            **Note** this looks like O(N^2) but .replies on clip
            comment will only ever have 1 item in it from paging response
            so it is really just O(N) on clipComments
         */
        if var clipWithReplyToDelete = clipComments.first(where: { $0.replies.first { $0.id == commentID } != nil }) {
            clipComments.remove(clipWithReplyToDelete)
            clipWithReplyToDelete.replies.removeAll { $0.id == commentID }
            clipComments.insert(clipWithReplyToDelete)
        }

        commentsMap.removeValue(forKey: commentID)
        prepItem()
    }

    init(clipRemoteID: String,
         nextPageCursor: String? = nil,
         clipComments: [ClipComment],
         allowComment: Bool,
         disableReason: String? = nil)
    {
        self.clipRemoteID = clipRemoteID
        self.nextPageCursor = nextPageCursor
        self.clipComments = Set(clipComments)
        self.allowComment = allowComment
        self.disableReason = disableReason
        prepItem()
    }
}

private extension ClipCommentsThread {
    mutating func prepItem() {
        var newCommentsMap: [String: ClipComment] = [:]
        var unparentedSet: Set<String> = []
        var parentedMap: [String: Set<String>] = [:]
        var newHasMoreCommentsMap: [String: Bool] = [:]

        for comment in clipComments {
            newCommentsMap[comment.id] = comment
            if let parentID = comment.parentID {
                var parentCommentSet: Set<String> = parentedMap[parentID] ?? []
                parentCommentSet.insert(comment.id)
                parentedMap[parentID] = parentCommentSet
            } else {
                unparentedSet.insert(comment.id)
                newHasMoreCommentsMap[comment.id] = replyNextPageCursor[comment.id] != nil
            }

            for reply in comment.replies {
                newCommentsMap[reply.id] = reply
                var parentCommentSet: Set<String> = parentedMap[comment.id] ?? []
                parentCommentSet.insert(reply.id)
                parentedMap[comment.id] = parentCommentSet
            }
        }

        hasMoreRepliesMap = newHasMoreCommentsMap
        commentsMap = newCommentsMap
        let unparentedComments: [ClipComment] = unparentedSet
            .compactMap { commentID in commentsMap[commentID] }
        unparentedCommentsInSortedOrder = unparentedComments
            .sorted(by: { $0.createdAt > $1.createdAt })
            .map { $0.id }

        var newReplyMap: [String: [String]] = [:]

        for (parentID, replySet) in parentedMap {
            let parentedSet = replySet
                .compactMap { commentID in commentsMap[commentID] }
            let newOrderedReplies: [String] = parentedSet
                .sorted(by: { $0.createdAt < $1.createdAt })
                .map { $0.id }
            /* Oldest First */
            newReplyMap[parentID] = newOrderedReplies
        }

        replyMapInSortedOrder = newReplyMap
    }
}
