import APIClient
import AVFoundation
import CommentsClient
import ComponentLibrary
import ComposableArchitecture
import FeatureBrandedAlert
import FeatureCaptions
import FeatureToasts
import Localization
import OmniPlayerClient
import StatsigClient
import SwiftUI
import Utilities

@Reducer
public struct CommentsSheetReducer<Item: Commentable> {
    @ObservableState
    public struct State: Equatable {
        var authorCaption: AuthorCaption?
        var currentUser: User
        var item: Item
        var isEntityAuthorCurrentUser: Bool
        var source: HooksFeedSource?

        var userCommentText: String = ""
        var userMentions: [CommentUserMention] = []
        var userMentionSearchSuggestions: [SimpleProfile] = []
        var hasStartedTypingSession: Bool = false // tracks if user started typing (for analytics)

        var elapsedTrackTime: CMTime
        var lockedTrackTime: CMTime? // locks upon user typing to prevent time drift

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

        var areCommentsEnabled: Bool {
            commentsAccessMap.areCommentsEnabledOnEntity(item.commentEntityId, entityType: item.commentEntityType)
        }

        var pendingComments: [CommentEntity] = []
        var comments: [CommentEntity] = []

        // Local state for comment routing
        var replyToCommentID: String?

        /*
            If currentReplyParentCommentID is not nil
            it means we are currently replying to a specific
            comment with this parent ID.

            If this is nil it means we are just adding an
            unparented root comment.
         */
        var currentReplyParentCommentID: String?
        var replyCommentsMap: [String: [CommentEntity]] = [:]
        var hasMoreRepliesMap: [String: Bool] = [:]
        var hasExpandedComment: [String: Bool] = [:] // Instead of state on item which apparently gets reset on liking a comment
        var totalCommentCount: CommentsTotalCountMap.CountStyle {
            commentsCountMap.commentCountForEntity(item.commentEntityId, entityType: item.commentEntityType)
        }

        var hasCommentText: Bool {
            !userCommentText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
        }

        var commentTrackTime: Double {
            lockedTrackTime?.seconds ?? elapsedTrackTime.seconds
        }

        var replyRootComment: CommentEntity? {
            comments.first { $0.id == currentReplyParentCommentID }
        }

        var hasReplyComment: Bool {
            replyRootComment != nil
        }

        var hasAuthorCaption: Bool {
            authorCaption != nil
        }

        var replyRootCommentUserDisplayName: String {
            guard let replyComment = replyRootComment else { return "" }
            return L10n.FeatureComments.replyTo(replyComment.userDisplayName)
        }

        var replyRootCommentContent: String {
            guard let replyComment = replyRootComment else { return "" }
            return replyComment.content
        }

        var replyParentCommentContent: String {
            replyRootComment?.content ?? ""
        }

        var infiniteScrollLimit: Int {
            max(0, comments.count - CommentsClientV2.infiniteScrollMarkerOffset)
        }

        var isLoading: Bool {
            isRequestingComments && comments.isEmpty
        }

        var noCommentsAndNoAuthorCaption: Bool {
            comments.isEmpty && !hasAuthorCaption
        }

        var isClipCommentsSheet: Bool {
            item.isClip
        }

        /*
            Spamming reactions is rate limited on backend
            and this is also a mechanism to limit spamming requests
         */
        var isRequestingComments: Bool = false

        /*
            In order to limit how many retriggers to fetch next page
            are triggered we keep track of the last requesting index

            retriggers need to meet the requirements of CommentsSheetClient
            && retriggers need to be higher than this at a minimum
         */
        var greatestRequestIndex: Int = .zero

        @ObservationStateIgnored @ObservedBox var toastState = ToastReducer.State()
        @ObservationStateIgnored @ObservedBox public var brandedAlert = BrandedAlert.State(style: .noAlert)

        public init(
            authorCaption: AuthorCaption? = nil,
            currentUser: User,
            item: Item,
            isEntityAuthorCurrentUser: Bool,
            comments: [CommentEntity] = [],
            replyCommentsMap: [String: [CommentEntity]] = [:],
            hasMoreRepliesMap: [String: Bool] = [:],
            elapsedTrackTime: CMTime = .zero,
            source: HooksFeedSource? = nil
        ) {
            self.authorCaption = authorCaption
            self.currentUser = currentUser
            self.item = item
            self.pendingComments = []
            self.comments = comments
            self.replyCommentsMap = replyCommentsMap
            self.hasMoreRepliesMap = hasMoreRepliesMap
            self.currentReplyParentCommentID = nil
            self.greatestRequestIndex = .zero
            self.isEntityAuthorCurrentUser = isEntityAuthorCurrentUser
            self.elapsedTrackTime = elapsedTrackTime
            self.lockedTrackTime = nil
            self.source = source
        }
    }

    public enum Action: BindableAction {
        // Lifecycle
        case task
        case onAppear

        // Event Handlers
        case commentsSheetClient(CommentsClientV2.Event)
        case omniplayerEvent(OmniPlayerEvent)

        // Comments Management
        case loadNextPageOfCommentsForEntity
        case loadNextReplyPageOfComments(String)
        case postComment
        // (commentID, entityId, entityType, reaction, incomingCount)
        case updateCommentReaction(String, String, CommentEntity.CommentEntityType, CommentReactionType, Int)
        // (commentID, entityId, entityType, reason)
        case reportComment(String, String, CommentEntity.CommentEntityType, ReportCommentRequest.Reason)
        // (commentID, entityId, entityType)
        case deleteComment(String, String, CommentEntity.CommentEntityType)

        // User Input
        case addSingleEmoji(String)
        case openUserMentionSearch
        case onUserCommentTextUpdated(oldValue: String, newValue: String)
        case onUserMentionSearchItemTapped(SimpleProfile)

        // UI State Changes
        case onItemAppeared(Int)
        case expandedComment(String)
        case setReplyTarget(String)
        case clearReplyTarget

        // Navigation
        case didTapUserHandle(String)
        case tapCommentTrackTimestamp(_ trackTimestamp: TimeInterval)
        case deeplinkIntoReplyForComment(_ commentId: String)

        // Child Reducers
        case toastAction(ToastReducer.Action)
        case brandedAlert(BrandedAlert.Action)

        // Internal
        case delegate(Delegate)
        case binding(BindingAction<State>)
        case `internal`(Internal)

        public enum Internal {
            case periodicTimeResponse(CMTime)
        }

        public enum Delegate {
            case dismiss
            /*
                We should separate delegate functions from those used by
                the reducer itself, this is not a duplicate.
             */
            case showUserProfile(_ handle: String)
        }
    }

    @Dependency(\.commentsClientV2) var commentsSheetClient

    public init() {}

    struct CommentsSheetClientEventCancellableId: Hashable {}
    struct ElapsedTrackTimeCancellableId: Hashable {}
    struct UserMentionSearchCancellableId: Hashable {}

    public var body: some ReducerOf<Self> {
        Scope(state: \.brandedAlert, action: \.brandedAlert) {
            BrandedAlert()
        }
        BindingReducer()
        Scope(state: \.toastState, action: \.toastAction) {
            ToastReducer()
        }
        Reduce { state, action in
            switch action {
            case .task:
                return subscribeToAllEvents()

            case .commentsSheetClient(let event):
                return handleCommentSheetClientEvent(event, state: &state)

            case .omniplayerEvent(let event):
                return handleOmniplayerClientEvent(event, state: &state)

            case .internal(.periodicTimeResponse(let time)):
                guard time.seconds.isFinite && state.elapsedTrackTime.seconds.isFinite else {
                    // Validate that time values are finite and not NaN.
                    // CMTime can become invalid in certain scenarios (ie. when audio/video playback encounters issues)
                    return .none
                }

                let didTimeChange = Int(time.seconds) != Int(state.elapsedTrackTime.seconds)
                guard didTimeChange else { return .none }
                state.elapsedTrackTime = time
                return .none

            case .onAppear:
                return .send(.loadNextPageOfCommentsForEntity)

            case .onItemAppeared(let index):
                guard
                    !state.isRequestingComments,
                    index >= state.infiniteScrollLimit,
                    index > state.greatestRequestIndex
                else { return .none }
                state.greatestRequestIndex = index
                state.isRequestingComments = true
                return .send(.loadNextPageOfCommentsForEntity)

            case .loadNextPageOfCommentsForEntity:
                state.isRequestingComments = true
                commentsSheetClient.getComments(state.item.commentEntityId, state.item.commentEntityType)
                return .none

            case .loadNextReplyPageOfComments(let commentID):
                commentsSheetClient.getCommentReplies(state.item.commentEntityId, state.item.commentEntityType, commentID)
                return .none

            case .expandedComment(let commentID):
                state.hasExpandedComment[commentID] = !(state.hasExpandedComment[commentID] ?? false)
                return .none

            case .setReplyTarget(let commentID):
                state.currentReplyParentCommentID = commentID
                lockTrackTimeIfNeeded(state: &state, when: state.hasCommentText)
                return .none

            case .clearReplyTarget:
                state.currentReplyParentCommentID = nil
                return .none

            case .addSingleEmoji(let emoji):
                return handleAddSingleEmoji(emoji, state: &state)

            case .onUserCommentTextUpdated(let oldValue, let newValue):
                return handleUserCommentTextUpdated(oldValue: oldValue, newValue: newValue, state: &state)

            case .binding(\.userCommentText):
                // iOS quickType/autocomplete bypasses .onChange and goes directly to .binding
                // Without this, timestamp locking would fail for autocomplete suggestions
                lockTrackTimeIfNeeded(state: &state, when: state.hasCommentText)
                unlockTrackTimeIfNeeded(state: &state, when: !state.hasCommentText)
                return .none

            case .openUserMentionSearch:
                return handleUserMentionSearch(state: &state)

            case .onUserMentionSearchItemTapped(let user):
                return handleUserMentionSearchItemTapped(user, state: &state)

            case .postComment:
                // If replying to a comment, inherit parent's entity type
                // Otherwise use the item's default entity type
                let entityType: CommentEntity.CommentEntityType
                if let parentCommentId = state.currentReplyParentCommentID,
                   let parentComment = state.comments.first(where: { $0.id == parentCommentId })
                {
                    // Reply inherits parent comment's entity type
                    entityType = parentComment.entityType
                } else {
                    // New comment uses the item's default entity type
                    entityType = state.item.commentEntityType
                }
                return handlePostComment(entityType: entityType, state: &state)

            case .updateCommentReaction(let commentID, let entityId, let entityType, let reaction, let incomingReactionCount):
                commentsSheetClient.setCommentReaction(entityId, entityType, commentID, reaction, incomingReactionCount)
                return .none

            case .reportComment(let commentID, let entityId, let entityType, let reason):
                commentsSheetClient.reportComment(entityId, entityType, commentID, reason)
                return .none

            case .deleteComment(let commentID, let entityId, let entityType):
                commentsSheetClient.deleteComment(entityId, entityType, commentID)
                return .none

            case .didTapUserHandle(let handle):
                return .send(.delegate(.showUserProfile(handle)))

            case .tapCommentTrackTimestamp(let trackTimestamp):
                @Dependency(\.omniplayerClient.seekToFromCommentTrackTimestamp) var seekToFromCommentTrackTimestamp
                guard state.item.isClip else { return .none }
                let seekTime = CMTime(seconds: trackTimestamp, preferredTimescale: 1000)
                seekToFromCommentTrackTimestamp(seekTime)
                return .none

            case .deeplinkIntoReplyForComment(let commentId):
                if let existingIndex = state.comments.firstIndex(where: { $0.id == commentId }) {
                    // Move comment to top and set as reply target
                    let comment = state.comments.remove(at: existingIndex)
                    state.comments.insert(comment, at: 0)
                    state.currentReplyParentCommentID = commentId
                    return .none
                } else {
                    // Comment not loaded, fetch it
                    state.replyToCommentID = commentId
                    state.isRequestingComments = true
                    commentsSheetClient.getComment(state.item.commentEntityId, state.item.commentEntityType, commentId)
                    return .none
                }

            case .toastAction, .brandedAlert, .delegate, .binding:
                return .none
            }
        }
        Analytics()
    }
}
