import APIClient
import Collections
import Foundation

actor ClipCommentsCache {
    private static let maxCacheCount: Int = 10
    private static let maxUsersPerThread: Int = 50
 
    private var commentsCache: [String: ClipCommentsThread] = [:]
    private var countsCache: [String: Int] = [:]
    private var usersInThreads: [String: [String]] = [:] // clipRemoteID -> user handles

    private var mostRecentCommentTrackerIndex: Int = .zero
    private var mostRecentCommentsSections = Array(repeating: "", count: ClipCommentsCache.maxCacheCount)

    func getComment(clipRemoteID: String, commentID: String) async -> ClipComment? {
        let cacheItem = commentsCache[clipRemoteID]
        return cacheItem?.commentById(commentID)
    }

    func getCurrentCommentsCache() async -> ClipCommentsThreadMap {
        var cacheCopy: [String: ClipCommentsThread] = [:]
        for item in commentsCache {
            cacheCopy[item.key] = item.value
        }
        return ClipCommentsThreadMap(commentsCache: cacheCopy)
    }

    func getCurrentCountCache() async -> ClipCommentTotalCountMap {
        var cacheCopy: [String: Int] = [:]
        for item in countsCache {
            cacheCopy[item.key] = item.value
        }
        return ClipCommentTotalCountMap(counts: cacheCopy)
    }

    func getCurrentAccessCache() async -> ClipCommentAccessMap {
        var accessCacheCopy: [String: Bool] = [:]
        for item in commentsCache {
            accessCacheCopy[item.key] = item.value.allowComment
        }
        return ClipCommentAccessMap(accessMap: accessCacheCopy)
    }

    func addUserToThread(_ clipRemoteID: String, _ userHandle: String) async {
        var users = OrderedSet(usersInThreads[clipRemoteID, default: []])
        users.append(userHandle) // OrderedSet prevents duplicates

        let finalUsers = Array(users)
        usersInThreads[clipRemoteID] = finalUsers

        // Keep only the most recent users
        if finalUsers.count > Self.maxUsersPerThread {
            usersInThreads[clipRemoteID] = Array(finalUsers.suffix(Self.maxUsersPerThread))
        }
    }

    func addUsersToThread(_ clipRemoteID: String, _ userHandles: [String]) async {
        var users = OrderedSet(usersInThreads[clipRemoteID, default: []])
        for userHandle in userHandles {
            users.append(userHandle) // OrderedSet prevents duplicates
        }

        let finalUsers = Array(users)
        usersInThreads[clipRemoteID] = finalUsers

        // Keep only the most recent users
        if finalUsers.count > Self.maxUsersPerThread {
            usersInThreads[clipRemoteID] = Array(finalUsers.suffix(Self.maxUsersPerThread))
        }
    }

    func getUsersInThread(_ clipRemoteID: String) async -> [String] {
        return usersInThreads[clipRemoteID] ?? []
    }

    func updateCountCacheWithClipID(_ clipRemoteID: String, commentCount: Int) async {
        countsCache[clipRemoteID] = commentCount
    }

    func updateCacheWithCommentsPage(clipRemoteID: String, page: CommentsPage) async {
        // Track users from comments
        let userHandles = page.results.map(\.userHandle)
        await addUsersToThread(clipRemoteID, userHandles)

        if var cacheItem = commentsCache[clipRemoteID] {
            cacheItem.addPage(page) // This is mutating and causes prepItem to run again
            commentsCache[clipRemoteID] = cacheItem
        } else {
            limitCache(clipRemoteID)
            commentsCache[clipRemoteID] = ClipCommentsThread(
                clipRemoteID: clipRemoteID,
                nextPageCursor: page.nextCursor,
                clipComments: page.results,
                allowComment: page.allowComment,
                disableReason: page.disableReason
            )
        }
    }

    func limitCache(_ newClipID: String) {
        let trackerIndex = mostRecentCommentTrackerIndex
        let oldClipID = mostRecentCommentsSections[trackerIndex]

        /*
             Remove the old entry from the cache if it exists
             and is not still referenced in the tracker array
         */
        if !oldClipID.isEmpty,
           oldClipID != newClipID,
           !mostRecentCommentsSections.contains(where: { $0 == oldClipID })
        {
            commentsCache.removeValue(forKey: oldClipID)
            usersInThreads.removeValue(forKey: oldClipID)
        }

        /* Update the tracker and move to the next index; circular */
        mostRecentCommentsSections[trackerIndex] = newClipID
        mostRecentCommentTrackerIndex = (trackerIndex + 1) % ClipCommentsCache.maxCacheCount
    }

    func updateCacheWithRepliesPage(_ clipRemoteID: String, commentID: String, page: CommentRepliesPage) async {
        // Track users from replies
        let userHandles = page.replies.map(\.userHandle)
        await addUsersToThread(clipRemoteID, userHandles)

        guard var cacheItem = commentsCache[clipRemoteID] else { return }
        cacheItem.addRepliesPage(page, rootCommentID: commentID) // This is mutating and causes prepItem to run again
        commentsCache[clipRemoteID] = cacheItem
    }

    func addNewCommentToCache(clipRemoteID: String, newComment: ClipComment) async {
        // Track user from new comment
        await addUserToThread(clipRemoteID, newComment.userHandle)

        if var cacheItem = commentsCache[clipRemoteID] {
            cacheItem.addComment(newComment) // This is mutating and causes prepItem to run again
            commentsCache[clipRemoteID] = cacheItem
        } else {
            /*
                Unlikely to reach this case

                Assumes this means there are no comments,
                As reaching an area where you are able to comment
                would mean first requesting the existing comments
             */
            commentsCache[clipRemoteID] = ClipCommentsThread(
                clipRemoteID: clipRemoteID,
                nextPageCursor: nil,
                clipComments: [newComment],
                allowComment: true,
                disableReason: nil
            )
        }
    }

    func updateReactionOnCommentInCache(
        clipRemoteID: String,
        commentID: String,
        reaction: CommentReactionType,
        numLikes: Int
    ) async {
        guard var cacheItem = commentsCache[clipRemoteID] else { return }
        // This is mutating and however DOES NOT cause prepItem to run again
        cacheItem.updateCommentReaciton(commentID, reaction, numLikes)
        commentsCache[clipRemoteID] = cacheItem
    }

    func updateCommentAbilityOnClip(_ clipRemoteID: String, canComment: Bool) async {
        guard var cacheItem = commentsCache[clipRemoteID] else { return }
        cacheItem.allowComment = canComment
        commentsCache[clipRemoteID] = cacheItem
    }

    func deleteCommentInCache(
        clipRemoteID: String,
        commentID: String
    ) async {
        guard var cacheItem = commentsCache[clipRemoteID] else { return }
        cacheItem.deleteComment(commentID)
        commentsCache[clipRemoteID] = cacheItem
    }

    func getCommentsCacheItem(_ clipRemoteID: String) async -> ClipCommentsThread? {
        return commentsCache[clipRemoteID]
    }

    func getCommentsCursorForClip(_ clipRemoteID: String) async -> String? {
        return commentsCache[clipRemoteID]?.nextPageCursor
    }

    func getReplyCursorForComment(_ commentID: String, clipRemoteID: String) async -> String? {
        return commentsCache[clipRemoteID]?.replyCursorForComment(commentID)
    }
}
