import APIClient
import AsyncAlgorithms
import Combine
import ComposableArchitecture
import Foundation
import Localization
import Utilities

public extension CommentsClientV2 {
    enum Event: Equatable {
        case didUpdateComments(CommentsSheetMap)
        case didReportComment(CommentEntity)

        case didResolveTemporaryComment(String)

        case didUpdateSuggestedUserMentions([SimpleProfile])

        case didReceiveAPIToastingError(String)
        case didReceiveAPIModalError(String)
    }
}

// TODO: Add `contexualEntityId` to to determine the API call to make for either hook or clip comments
@DependencyClient
public struct CommentsClientV2 {
    public var stream: () -> AsyncStream<CommentsClientV2.Event> = { .never }

    public var setCommentsActivationState: (_ state: CommentsSheetActivationState) -> Void
    public var setCommentsCount: (_ count: Int, _ entityId: String, _ entityType: CommentEntity.CommentEntityType) -> Void

    public var getComments: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType) -> Void
    public var getComment: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ commentID: String) -> Void

    public var postComment: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ temporaryCommentID: String, _ parentCommentID: String?, _ trackTimestamp: Double?, _ content: String, _ userMentions: [UserMention], _ recommendationMetadata: HooksRecommendationMetadata?) -> Void
    public var setCommentReaction: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ commentID: String, _ reaction: CommentReactionType, _ incomingReactionCount: Int) -> Void
    public var deleteComment: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ commentID: String) -> Void
    public var reportComment: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ commentID: String, _ reportReason: ReportCommentRequest.Reason) -> Void
    public var getCommentReplies: (_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ commentID: String) -> Void

    public var searchForUserByHandle: (_ handle: String, _ entityId: String, _ entityType: CommentEntity.CommentEntityType) -> Void
}

extension CommentsClientV2: DependencyKey {
    public static var liveValue: CommentsClientV2 {
        @Dependency(APIClientV2.self) var apiClientV2

        @Shared(.inMemory(.commentsSheetAccessMap)) var commentsAccessMap: CommentsAccessMap = .defaultValue
        @Shared(.inMemory(.commentsSheetCountMap)) var commentsCountMap: CommentsTotalCountMap = .defaultValue
        @Shared(.inMemory(.commentsSheetActivationState)) var commentsActivationState: CommentsSheetActivationState = .notActive

        let subject = PassthroughSubject<CommentsClientV2.Event, Never>()

        let commentsCache = CommentsSheetCache()

        let pageSize = CommentsClientV2.pageSize
        let sortOrder = CommentsClientV2.sortOrder

        func _internal_sendCommentsValueUpdateEvent() async {
            let commentsCacheValue = await commentsCache.getCurrentCommentsCache()
            let newAccessMap = await commentsCache.getCurrentAccessCache()
            let newCountMap = await commentsCache.getCurrentCountCache()

            Task { @MainActor in
                $commentsAccessMap.withLock { $0 = newAccessMap }
                $commentsCountMap.withLock { $0 = newCountMap }
                subject.send(.didUpdateComments(commentsCacheValue))
            }
        }

        func _internal_updateCacheWithCommentCountsWithoutSendingEvent(_ entityId: String, _ entityType: CommentEntity.CommentEntityType) async throws {
            let commentCount: CommentCountResponse = try await apiClientV2.getHookCommentCount(entityId)
            await commentsCache.updateCountCacheWithEntity(entityId, entityType: entityType, commentCount: commentCount.count)
        }

        func _internal_forceCommentCountsWithoutSendingEvent(_ entityId: String, _ entityType: CommentEntity.CommentEntityType, _ forcedCount: Int) async throws {
            await commentsCache.updateCountCacheWithEntity(entityId, entityType: entityType, commentCount: forcedCount)
        }

        return Self(
            stream: {
                AsyncStream { continuation in
                    let cancellable = subject.sink { event in
                        continuation.yield(event)
                    }
                    continuation.onTermination = { _ in
                        cancellable.cancel()
                    }
                }
            },
            setCommentsActivationState: { newState in
                Task { @MainActor in
                    $commentsActivationState.withLock { $0 = newState }
                }
            },
            setCommentsCount: { count, entityId, entityType in
                Task { @MainActor in
                    do {
                        try await _internal_forceCommentCountsWithoutSendingEvent(entityId, entityType, count)
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            },
            getComments: { entityId, entityType in
                Task {
                    do {
                        try await withThrowingTaskGroup(of: Void.self) { group in
                            // Gets comments
                            group.addTask {
                                let pageCursor = await commentsCache.getCommentsCursorForEntity(entityId, entityType: .hook)
                                let page: CommentsSheetPage = try await apiClientV2.getHookComments(
                                    entityId, pageCursor, pageSize, sortOrder
                                )
                                await commentsCache.updateCacheWithCommentsPage(entityId, entityType: .hook, page: page)
                            }

                            // Get commments count
                            group.addTask {
                                try await _internal_updateCacheWithCommentCountsWithoutSendingEvent(entityId, entityType)
                            }

                            // Await for both tasks to complete
                            try await group.waitForAll()
                        }

                        await _internal_sendCommentsValueUpdateEvent()
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            },
            getComment: { entityId, entityType, commentID in
                Task {
                    // Check if comment already exists
                    let existingComment = await commentsCache.getComment(entityId: entityId, entityType: .hook, commentID: commentID)
                    if existingComment != nil {
                        // Comment already cached, just trigger update event to reposition in UI
                        await _internal_sendCommentsValueUpdateEvent()
                        return
                    }

                    // Fetch comment and the first page of replies
                    do {
                        async let getCommentTask = apiClientV2.getHookComment(entityId, commentID)
                        async let getRepliesTask: [CommentEntity] = {
                            do {
                                let repliesResponse = try await apiClientV2.getHookCommentReplies(
                                    commentID, entityType, nil, CommentsClientV2.replyPageSize
                                )
                                return repliesResponse.replies
                            } catch {
                                log.telemetry.assertionFailure("Failed to fetch \(entityType.rawValue) replies for comment (\(commentID))")
                                return []
                            }
                        }()
                        let (targetComment, replies) = try await(getCommentTask, getRepliesTask)

                        await commentsCache.addNewCommentToCache(entityId, entityType: .hook, newComment: targetComment)
                        for replyComment in replies {
                            await commentsCache.addNewCommentToCache(entityId, entityType: .hook, newComment: replyComment)
                        }
                        await _internal_sendCommentsValueUpdateEvent()
                    } catch {
                        log.telemetry.assertionFailure("Failed to fetch \(entityType.rawValue) comment (\(commentID))")
                    }
                }
            },
            postComment: { entityId, entityType, temporaryCommentID, parentCommentID, trackTimestamp, userCommentText, userMentions, recommendationMetadataSchema in
                Task {
                    do {
                        let postedComment: CommentEntity = try await apiClientV2.postHookComment(
                            entityId,
                            entityType,
                            userCommentText,
                            parentCommentID,
                            trackTimestamp,
                            userMentions,
                            recommendationMetadataSchema
                        )
                        await commentsCache.addNewCommentToCache(entityId, entityType: .hook, newComment: postedComment)
                    } catch {
                        /// Rollback the pending comment by deleting it
                        subject.send(.didResolveTemporaryComment(temporaryCommentID))

                        /// inspect the error to show a toast, letting the user know why the comment didn't post
                        switch error {
                        case let apiError as APIError:
                            switch apiError {
                            case .forbidden:
                                // 403 is in comment jail. show the full blocking modal
                                subject.send(.didReceiveAPIModalError(apiError.errorDetail ?? L10n.FeatureComments.jailDescription))
                            default:
                                subject.send(.didReceiveAPIToastingError(apiError.errorDetail ?? L10n.FeatureComments.commentPostErrorGeneric))
                            }

                        default:
                            subject.send(.didReceiveAPIToastingError(L10n.FeatureComments.commentPostErrorGeneric))
                            log.telemetry.error(error, message: "Unexpected error when posting comment.")
                        }

                        /// Bail out the rest of the comment sync handling
                        return
                    }

                    do {
                        try await _internal_updateCacheWithCommentCountsWithoutSendingEvent(entityId, entityType)
                    } catch {
                        log.telemetry.error(error, message: "Unexpected error when posting comment. Possibly failed to get count.")
                    }
                    subject.send(.didResolveTemporaryComment(temporaryCommentID))
                    await _internal_sendCommentsValueUpdateEvent()
                }
            },
            setCommentReaction: { entityId, entityType, commentID, reactionType, incomingReactionType in
                Task {
                    do {
                        // Immediately update
                        await commentsCache.updateReactionOnCommentInCache(
                            entityId,
                            entityType: .hook,
                            commentID: commentID,
                            reaction: reactionType,
                            numLikes: reactionType.countValueDelta + incomingReactionType
                        )
                        await _internal_sendCommentsValueUpdateEvent()

                        // Attempt to update remote value
                        try await apiClientV2.setHookCommentReaction(commentID, entityType, reactionType == .like)

                        await _internal_sendCommentsValueUpdateEvent()
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            },
            deleteComment: { entityId, entityType, commentID in
                Task {
                    do {
                        try await apiClientV2.deleteHookComment(commentID, entityType)

                        await commentsCache.deleteCommentInCache(entityId, entityType: .hook, commentID: commentID)
                        try await _internal_updateCacheWithCommentCountsWithoutSendingEvent(entityId, entityType)
                        await _internal_sendCommentsValueUpdateEvent()
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            },
            reportComment: { entityId, entityType, commentID, reportReason in
                Task {
                    do {
                        _ = try await apiClientV2.reportHookComment(commentID, entityType, CommentReportingBody(reason: reportReason.rawValue))

                        let reportedComment = await commentsCache.getComment(entityId: entityId, entityType: .hook, commentID: commentID)
                        guard let reportedComment else { return }
                        subject.send(.didReportComment(reportedComment))
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            },
            getCommentReplies: { entityId, entityType, commentID in
                Task {
                    do {
                        let pageCursor = await commentsCache.getReplyCursorForComment(commentID, entityId: entityId, entityType: .hook)

                        let comment = await commentsCache.getComment(entityId: entityId, entityType: .hook, commentID: commentID)
                        let commentEntityType = comment?.entityType ?? entityType

                        var page: CommentSheetRepliesPage = try await apiClientV2.getHookCommentReplies(
                            commentID, commentEntityType, pageCursor, CommentsClientV2.replyPageSize
                        )

                        page.updatePageWithEntityAndParentId(for: entityType, withEntityId: entityId, parentId: commentID)
                        await commentsCache.updateCacheWithReplies(entityId, entityType: .hook, commentID: commentID, page: page)
                        await _internal_sendCommentsValueUpdateEvent()
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            },
            searchForUserByHandle: { handle, entityId, entityType in
                Task {
                    do {
                        let usersInThread = await commentsCache.getUsersInThread(entityId, entityType)

                        let matchingUsers = try await apiClientV2.searchUsers(
                            usersInThread.isEmpty ? nil : usersInThread,
                            nil,
                            handle
                        )
                        subject.send(.didUpdateSuggestedUserMentions(matchingUsers))
                    } catch {
                        log.telemetry.error(error)
                    }
                }
            }
        )
    }
}

// MARK: - Dependency Values

public extension DependencyValues {
    var commentsClientV2: CommentsClientV2 {
        get { self[CommentsClientV2.self] }
        set { self[CommentsClientV2.self] = newValue }
    }
}

extension CommentsClientV2: TestDependencyKey {
    public static var previewValue: CommentsClientV2 { noop }
    public static var testValue: CommentsClientV2 { Self() }
}

public extension CommentsClientV2 {
    static var noop: Self { Self() }
}
