package app.closer.ui.messages

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.closer.core.analytics.AnalyticsTracker
import app.closer.core.billing.CouplePremiumChecker
import app.closer.data.local.QuestionDao
import app.closer.data.local.mapper.toQuestion
import app.closer.domain.model.QuestionMessage
import app.closer.domain.repository.ConversationRepository
import app.closer.domain.repository.CoupleRepository
import app.closer.domain.repository.UserRepository
import app.closer.notifications.ActiveThreadMonitor
import com.google.firebase.auth.FirebaseAuth
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject

/** A media send in flight (or failed) — shown as a chip above the composer, not in the message list. */
data class PendingMedia(val id: String, val type: String, val failed: Boolean = false)

data class ConversationUiState(
    val title: String = "",
    val partnerPhotoUrl: String? = null,
    val messages: List<QuestionMessage> = emptyList(),
    val pendingMedia: List<PendingMedia> = emptyList(),
    val canLoadMore: Boolean = false,
    val partnerReadAt: Long = 0L,
    val partnerTyping: Boolean = false,
    /** Couple-shared premium: gates SENDING media + reactions (false until confirmed). */
    val canSendMedia: Boolean = false,
    val messageInput: String = "",
    val isLoading: Boolean = true
)

@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class ConversationViewModel @Inject constructor(
    private val repository: ConversationRepository,
    private val coupleRepository: CoupleRepository,
    private val userRepository: UserRepository,
    private val questionDao: QuestionDao,
    private val activeThreadMonitor: ActiveThreadMonitor,
    private val couplePremiumChecker: CouplePremiumChecker,
    private val analyticsTracker: AnalyticsTracker,
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val coupleId: String = savedStateHandle["coupleId"] ?: ""
    private val conversationId: String = savedStateHandle["conversationId"] ?: ""
    val currentUserId: String = FirebaseAuth.getInstance().currentUser?.uid ?: ""

    private val _uiState = MutableStateFlow(ConversationUiState())
    val uiState: StateFlow<ConversationUiState> = _uiState.asStateFlow()

    /** One-shot, non-sticky messages for the snackbar (send failures). */
    private val _events = MutableSharedFlow<String>(extraBufferCapacity = 4)
    val events: SharedFlow<String> = _events.asSharedFlow()

    private val messageLimit = MutableStateFlow(PAGE_SIZE)

    private data class RetryData(val type: String, val bytes: ByteArray, val durationMs: Long)
    private val retryStore = mutableMapOf<String, RetryData>()

    init {
        // Reading this conversation suppresses its bubble + clears its unread.
        activeThreadMonitor.enter(conversationId)
        load()
    }

    override fun onCleared() {
        super.onCleared()
        activeThreadMonitor.leave(conversationId)
        stopTyping() // fire-and-forget; clears our typing flag when leaving
    }

    private fun load() {
        viewModelScope.launch {
            if (conversationId == "main") {
                runCatching { repository.ensureMainConversation(coupleId) }
            } else if (conversationId.startsWith("q_")) {
                runCatching { repository.ensureQuestionConversation(coupleId, conversationId.removePrefix("q_")) }
            }

            // Title + partner avatar.
            val couple = runCatching { coupleRepository.getCoupleForUser(currentUserId) }.getOrNull()
            val partnerId = couple?.userIds?.firstOrNull { it != currentUserId }
            val partner = partnerId?.let { runCatching { userRepository.getUser(it) }.getOrNull() }
            val title = if (conversationId.startsWith("q_")) {
                val questionId = conversationId.removePrefix("q_")
                runCatching { questionDao.getQuestionById(questionId)?.toQuestion()?.text }.getOrNull()
                    ?: "Discussion"
            } else {
                partner?.displayName?.takeIf { it.isNotBlank() } ?: "Your conversation"
            }
            _uiState.update {
                it.copy(title = title, partnerPhotoUrl = partner?.photoUrl, isLoading = false)
            }

            // Couple-shared premium gate for sending media + reactions.
            launch {
                couplePremiumChecker.coupleHasPremium(partnerId)
                    .collect { premium -> _uiState.update { it.copy(canSendMedia = premium) } }
            }

            runCatching { repository.markRead(coupleId, conversationId, currentUserId) }

            launch {
                messageLimit
                    .flatMapLatest { limit -> repository.observeMessages(coupleId, conversationId, limit) }
                    .collect { msgs ->
                        _uiState.update { it.copy(messages = msgs, canLoadMore = msgs.size >= messageLimit.value) }
                        runCatching { repository.markRead(coupleId, conversationId, currentUserId) }
                    }
            }

            launch {
                repository.observePartnerReadAt(coupleId, conversationId, currentUserId)
                    .collect { readAt -> _uiState.update { it.copy(partnerReadAt = readAt) } }
            }

            // Partner typing: combine the typing timestamp with a ticker so it auto-hides after ~6s
            // even if the partner's "stop" write never arrives (crash safety net).
            launch {
                combine(
                    repository.observePartnerTyping(coupleId, conversationId, currentUserId),
                    flow { while (true) { emit(Unit); delay(2000) } }
                ) { typingAt, _ -> typingAt > 0 && System.currentTimeMillis() - typingAt < TYPING_TTL_MS }
                    .distinctUntilChanged()
                    .collect { typing -> _uiState.update { it.copy(partnerTyping = typing) } }
            }
        }
    }

    private var typingStopJob: Job? = null
    private var lastTypingPingAt = 0L

    private fun onUserTyping() {
        val now = System.currentTimeMillis()
        if (now - lastTypingPingAt > 1500) {
            lastTypingPingAt = now
            repository.setTyping(coupleId, conversationId, currentUserId, true)
        }
        typingStopJob?.cancel()
        typingStopJob = viewModelScope.launch {
            delay(3000)
            stopTyping()
        }
    }

    private fun stopTyping() {
        typingStopJob?.cancel()
        lastTypingPingAt = 0L
        repository.setTyping(coupleId, conversationId, currentUserId, false)
    }

    /** Grow the live window by another page (triggered when scrolled to the top). */
    fun loadOlderMessages() {
        if (_uiState.value.canLoadMore) messageLimit.update { it + PAGE_SIZE }
    }

    fun updateMessageInput(text: String) {
        _uiState.update { it.copy(messageInput = text.take(MAX_MESSAGE_LENGTH)) }
        if (text.isNotBlank()) onUserTyping()
    }

    fun sendMessage() {
        val text = _uiState.value.messageInput.trim()
        if (text.isBlank() || currentUserId.isEmpty()) return
        _uiState.update { it.copy(messageInput = "") }
        stopTyping()
        viewModelScope.launch {
            runCatching { repository.sendMessage(coupleId, conversationId, currentUserId, text) }
                .onFailure { _events.tryEmit("Couldn't send message. Check your connection.") }
        }
    }

    fun sendImage(imageBytes: ByteArray) {
        if (currentUserId.isEmpty() || imageBytes.isEmpty()) return
        val id = UUID.randomUUID().toString()
        retryStore[id] = RetryData("image", imageBytes, 0L)
        addPending(PendingMedia(id, "image"))
        viewModelScope.launch {
            runCatching { repository.sendImageMessage(coupleId, conversationId, currentUserId, imageBytes) }
                .onSuccess { removePending(id); retryStore.remove(id) }
                .onFailure { markPendingFailed(id); _events.tryEmit("Couldn't send photo. Tap retry.") }
        }
    }

    fun sendVoice(audioBytes: ByteArray, durationMs: Long) {
        if (currentUserId.isEmpty() || audioBytes.isEmpty()) return
        val id = UUID.randomUUID().toString()
        retryStore[id] = RetryData("voice", audioBytes, durationMs)
        addPending(PendingMedia(id, "voice"))
        viewModelScope.launch {
            runCatching { repository.sendVoiceMessage(coupleId, conversationId, currentUserId, audioBytes, durationMs) }
                .onSuccess { removePending(id); retryStore.remove(id) }
                .onFailure { markPendingFailed(id); _events.tryEmit("Couldn't send voice note. Tap retry.") }
        }
    }

    /** Toggle the caller's emoji reaction on a message (tap the same emoji again to remove). */
    fun react(messageId: String, emoji: String) {
        if (currentUserId.isEmpty()) return
        val current = _uiState.value.messages.firstOrNull { it.id == messageId }?.reactions?.get(currentUserId)
        val next = if (current == emoji) null else emoji
        viewModelScope.launch {
            runCatching { repository.setReaction(coupleId, conversationId, messageId, currentUserId, next) }
                .onFailure { _events.tryEmit("Couldn't add reaction.") }
        }
    }

    /** Called when a gated media/reaction action routes the user to the paywall. */
    fun onMediaPaywallShown() {
        analyticsTracker.trackPaywallViewed("chat_media")
    }

    fun deleteMessage(messageId: String) {
        viewModelScope.launch {
            runCatching { repository.deleteMessage(coupleId, conversationId, messageId) }
                .onFailure { _events.tryEmit("Couldn't delete message.") }
        }
    }

    fun retryMedia(id: String) {
        val data = retryStore[id] ?: return
        removePending(id)
        retryStore.remove(id)
        when (data.type) {
            "image" -> sendImage(data.bytes)
            "voice" -> sendVoice(data.bytes, data.durationMs)
        }
    }

    fun dismissPending(id: String) {
        removePending(id)
        retryStore.remove(id)
    }

    suspend fun loadDecryptedMedia(mediaUrl: String): ByteArray? =
        repository.loadDecryptedMedia(coupleId, mediaUrl)

    private fun addPending(p: PendingMedia) =
        _uiState.update { it.copy(pendingMedia = it.pendingMedia + p) }

    private fun removePending(id: String) =
        _uiState.update { it.copy(pendingMedia = it.pendingMedia.filterNot { m -> m.id == id }) }

    private fun markPendingFailed(id: String) =
        _uiState.update { it.copy(pendingMedia = it.pendingMedia.map { m -> if (m.id == id) m.copy(failed = true) else m }) }

    companion object {
        const val MAX_MESSAGE_LENGTH = 2000
        const val PAGE_SIZE = 50
        private const val TYPING_TTL_MS = 6000L
    }
}
