package org.stepik.android.presentation.comment

import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.rxkotlin.Singles
import io.reactivex.rxkotlin.plusAssign
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.subjects.PublishSubject
import org.stepic.droid.di.qualifiers.BackgroundScheduler
import org.stepic.droid.di.qualifiers.MainScheduler
import ru.nobird.android.domain.rx.emptyOnErrorStub
import org.stepik.android.domain.comment.interactor.CommentInteractor
import org.stepik.android.domain.comment.interactor.ComposeCommentInteractor
import org.stepik.android.domain.comment.model.CommentsData
import org.stepik.android.domain.discussion_proxy.interactor.DiscussionProxyInteractor
import org.stepik.android.domain.discussion_proxy.model.DiscussionOrder
import org.stepik.android.domain.profile.interactor.ProfileGuestInteractor
import org.stepik.android.model.Step
import org.stepik.android.model.Submission
import org.stepik.android.model.comments.Comment
import org.stepik.android.model.comments.DiscussionProxy
import org.stepik.android.model.comments.Vote
import org.stepik.android.presentation.base.PresenterBase
import org.stepik.android.presentation.comment.mapper.CommentsStateMapper
import org.stepik.android.presentation.comment.model.CommentItem
import org.stepik.android.view.injection.step.StepDiscussionBus
import ru.nobird.android.core.model.PaginationDirection
import javax.inject.Inject

class CommentsPresenter
@Inject
constructor(
    private val commentInteractor: CommentInteractor,
    private val composeCommentInteractor: ComposeCommentInteractor,
    private val discussionProxyInteractor: DiscussionProxyInteractor,
    private val profileGuestInteractor: ProfileGuestInteractor,

    private val commentsStateMapper: CommentsStateMapper,

    @StepDiscussionBus
    private val stepDiscussionSubject: PublishSubject<Long>,

    @BackgroundScheduler
    private val backgroundScheduler: Scheduler,
    @MainScheduler
    private val mainScheduler: Scheduler
) : PresenterBase<CommentsView>() {
    private var state: CommentsView.State = CommentsView.State.Idle
        set(value) {
            field = value
            view?.setState(value)
        }

    override fun attachView(view: CommentsView) {
        super.attachView(view)
        view.setState(state)
    }

    /**
     * Data initialization variants
     */
    fun onDiscussion(discussionProxyId: String, discussionId: Long?, forceUpdate: Boolean = false) {
        if (state != CommentsView.State.Idle &&
            !((state == CommentsView.State.NetworkError || state is CommentsView.State.DiscussionLoaded) && forceUpdate)
        ) {
            return
        }

        val discussionOrderSource = (state as? CommentsView.State.DiscussionLoaded)
            ?.discussionOrder
            ?.let { Single.just(it) }
            ?: discussionProxyInteractor.getDiscussionOrder()

        compositeDisposable.clear()
        state = CommentsView.State.Loading
        compositeDisposable += Singles
            .zip(
                profileGuestInteractor.isGuest(),
                discussionProxyInteractor.getDiscussionProxy(discussionProxyId),
                discussionOrderSource
            )
            .observeOn(mainScheduler)
            .subscribeOn(backgroundScheduler)
            .subscribeBy(
                onSuccess = { (isGuest, discussionProxy, discussionOrder) ->
                    fetchComments(isGuest, discussionProxy, discussionOrder, discussionId)
                },
                onError = { state = CommentsView.State.NetworkError }
            )
    }

    private fun fetchComments(
        isGuest: Boolean,
        discussionProxy: DiscussionProxy,
        discussionOrder: DiscussionOrder,
        discussionId: Long?,
        keepCachedComments: Boolean = false
    ) {
        if (discussionProxy.discussions.isEmpty()) {
            state = CommentsView.State.DiscussionLoaded(isGuest, discussionProxy, discussionOrder, discussionId, CommentsView.CommentsState.EmptyComments)
        } else {
            val cachedComments: List<CommentItem.Data> = ((state as? CommentsView.State.DiscussionLoaded)
                ?.commentsState as? CommentsView.CommentsState.Loaded)
                ?.commentDataItems
                ?.takeIf { keepCachedComments }
                ?: emptyList()

            val newState = CommentsView.State.DiscussionLoaded(isGuest, discussionProxy, discussionOrder, discussionId, CommentsView.CommentsState.Loading)
            state = newState
            compositeDisposable += commentInteractor
                .getComments(discussionProxy, discussionOrder, discussionId, cachedComments)
                .observeOn(mainScheduler)
                .subscribeOn(backgroundScheduler)
                .subscribeBy(
                    onSuccess = {
                        state = newState.copy(commentsState = CommentsView.CommentsState.Loaded(it, commentsStateMapper.mapCommentDataItemsToRawItems(it)))
                        if (discussionId != null) {
                            view?.focusDiscussion(discussionId)
                        }
                    },
                    onError = { state = CommentsView.State.NetworkError }
                )
        }
    }

    /**
     * Discussion ordering
     */
    fun changeDiscussionOrder(discussionOrder: DiscussionOrder) {
        val oldState = (state as? CommentsView.State.DiscussionLoaded)
            ?: return

        compositeDisposable += discussionProxyInteractor
            .saveDiscussionOrder(discussionOrder)
            .observeOn(mainScheduler)
            .subscribeOn(backgroundScheduler)
            .subscribeBy(onError = emptyOnErrorStub)
        fetchComments(oldState.isGuest, oldState.discussionProxy, discussionOrder, oldState.discussionId, keepCachedComments = true)
    }

    /**
     * Load more logic in both directions
     */
    fun onLoadMore(direction: PaginationDirection) {
        val oldState = (state as? CommentsView.State.DiscussionLoaded)
            ?: return

        val commentsState = (oldState.commentsState as? CommentsView.CommentsState.Loaded)
            ?: return

        val commentDataItems =
            commentsState.commentDataItems

        val lastCommentId =
            when (direction) {
                PaginationDirection.PREV ->
                    commentDataItems
                        .takeIf { it.hasPrev }
                        .takeIf { commentsState.commentItems.first() !is CommentItem.Placeholder }
                        ?.first()
                        ?.id
                        ?: return

                PaginationDirection.NEXT ->
                    commentDataItems
                        .takeIf { it.hasNext }
                        .takeIf { commentsState.commentItems.last() !is CommentItem.Placeholder }
                        ?.last { it.comment.parent == null }
                        ?.id
                        ?: return
            }

        state = oldState.copy(commentsState = commentsStateMapper.mapToLoadMoreState(commentsState, direction))
        compositeDisposable += commentInteractor
            .getMoreComments(oldState.discussionProxy, oldState.discussionOrder, direction, lastCommentId)
            .observeOn(mainScheduler)
            .subscribeOn(backgroundScheduler)
            .subscribeBy(
                onSuccess = { state = commentsStateMapper.mapFromLoadMoreToSuccess(state, it, direction) },
                onError = { state = commentsStateMapper.mapFromLoadMoreToError(state, direction); view?.showNetworkError() }
            )
    }

    /**
     * Load more comments logic
     */
    fun onLoadMoreReplies(loadMoreReplies: CommentItem.LoadMoreReplies) {
        val oldState = (state as? CommentsView.State.DiscussionLoaded)
            ?: return

        val commentsState = (oldState.commentsState as? CommentsView.CommentsState.Loaded)
            ?: return

        state = oldState.copy(commentsState = commentsStateMapper.mapToLoadMoreRepliesState(commentsState, loadMoreReplies))
        compositeDisposable += commentInteractor
            .getMoreReplies(loadMoreReplies.parentComment, loadMoreReplies.lastCommentId)
            .observeOn(mainScheduler)
            .subscribeOn(backgroundScheduler)
            .subscribeBy(
                onSuccess = { state = commentsStateMapper.mapFromLoadMoreRepliesToSuccess(state, it, loadMoreReplies) },
                onError = { state = commentsStateMapper.mapFromLoadMoreRepliesToError(state, loadMoreReplies); view?.showNetworkError() }
            )
    }

    /**
     * Vote logic
     *
     * if [voteValue] is equal to current value new value will be null
     */
    fun onChangeVote(commentDataItem: CommentItem.Data, voteValue: Vote.Value) {
        if (commentDataItem.voteStatus !is CommentItem.Data.VoteStatus.Resolved ||
            commentDataItem.comment.actions?.vote != true) {
            return
        }

        val oldState = (state as? CommentsView.State.DiscussionLoaded)
            ?: return

        val commentsState = (oldState.commentsState as? CommentsView.CommentsState.Loaded)
            ?: return

        val newVote = commentDataItem.voteStatus.vote
            .copy(value = voteValue.takeIf { it != commentDataItem.voteStatus.vote.value })

        state = oldState.copy(commentsState = commentsStateMapper.mapToVotePending(commentsState, commentDataItem))
        compositeDisposable += commentInteractor
            .changeCommentVote(commentDataItem.id, newVote)
            .observeOn(mainScheduler)
            .subscribeOn(backgroundScheduler)
            .subscribeBy(
                onSuccess = { state = commentsStateMapper.mapFromVotePendingToSuccess(state, it) },
                onError = { state = commentsStateMapper.mapFromVotePendingToError(state, commentDataItem.voteStatus.vote); view?.showNetworkError() }
            )
    }

    fun onComposeCommentClicked(step: Step, parent: Long? = null, comment: Comment? = null, submission: Submission? = null) {
        val oldState = (state as? CommentsView.State.DiscussionLoaded)
            ?: return

        if (oldState.isGuest) {
            view?.showAuthRequired()
        } else {
            view?.showCommentComposeDialog(step, parent, comment, submission)
        }
    }

    /**
     * Edit logic
     */
    fun onCommentCreated(commentsData: CommentsData) {
        val commentDataItem = commentInteractor
            .mapCommentsDataToCommentItem(commentsData)
            ?: return

        state =
            if (commentDataItem.comment.parent != null) {
                commentsStateMapper.insertCommentReply(state, commentDataItem)
            } else {
                stepDiscussionSubject.onNext(commentDataItem.comment.target)
                commentsStateMapper.insertComment(state, commentDataItem)
            }
        view?.focusDiscussion(commentDataItem.id)
    }

    fun onCommentUpdated(commentsData: CommentsData) {
        val commentDataItem = commentInteractor
            .mapCommentsDataToCommentItem(commentsData)
            ?: return

        state = commentsStateMapper.mapFromVotePendingToSuccess(state, commentDataItem)
    }

    fun removeComment(commentId: Long) {
        val oldState = (state as? CommentsView.State.DiscussionLoaded)
            ?: return

        val commentsState = (oldState.commentsState as? CommentsView.CommentsState.Loaded)
            ?: return

        val commentDataItem = commentsState.commentDataItems.find { it.id == commentId }
            ?: return

        state = oldState.copy(commentsState = commentsStateMapper.mapToRemovePending(commentsState, commentDataItem))
        compositeDisposable += composeCommentInteractor
            .removeComment(commentDataItem.id)
            .observeOn(mainScheduler)
            .subscribeOn(backgroundScheduler)
            .doOnComplete {
                if (commentDataItem.comment.parent == null) {
                    stepDiscussionSubject.onNext(commentDataItem.comment.target)
                }
            }
            .subscribeBy(
                onComplete = { state = commentsStateMapper.mapFromRemovePendingToSuccess(state, commentId) },
                onError = { state = commentsStateMapper.mapFromRemovePendingToError(state, commentDataItem); view?.showNetworkError() }
            )
    }
}