package app.closer.data.remote

import app.closer.crypto.CoupleEncryptionManager
import app.closer.crypto.FieldEncryptor
import app.closer.domain.model.Conversation
import app.closer.domain.model.QuestionMessage
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.SetOptions
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
 * Backs the Messages inbox + each conversation's chat. Every conversation lives under
 * couples/{coupleId}/conversations/{conversationId} with a `messages` subcollection. All message
 * content (text + images) and the inbox `lastMessagePreview` are E2E-encrypted with the couple key
 * — the server only ever sees ciphertext. Mirrors the encryption used by the question threads.
 */
@Singleton
class FirestoreConversationDataSource @Inject constructor(
    private val db: FirebaseFirestore,
    private val encryptionManager: CoupleEncryptionManager,
    private val fieldEncryptor: FieldEncryptor,
    private val storageDataSource: FirebaseStorageDataSource
) {

    private fun conversationsRef(coupleId: String) =
        db.collection(FirestoreCollections.COUPLES).document(coupleId)
            .collection(FirestoreCollections.Couples.CONVERSATIONS)

    private fun messagesRef(coupleId: String, conversationId: String) =
        conversationsRef(coupleId).document(conversationId)
            .collection(FirestoreCollections.Conversations.MESSAGES)

    fun questionConversationId(questionId: String) = "q_$questionId"

    // ─── Conversations (inbox) ─────────────────────────────────────────────────────

    fun observeConversations(coupleId: String, currentUserId: String): Flow<List<Conversation>> = callbackFlow {
        val listener = conversationsRef(coupleId)
            .addSnapshotListener { snap, err ->
                if (err != null || snap == null) return@addSnapshotListener
                val aead = encryptionManager.aeadFor(coupleId)
                trySend(snap.documents.map { it.toConversation(aead, coupleId, currentUserId) })
            }
        awaitClose { listener.remove() }
    }

    /** Creates the pinned free-form couple conversation if it doesn't exist yet. */
    suspend fun ensureMainConversation(coupleId: String) {
        val ref = conversationsRef(coupleId).document(FirestoreCollections.Conversations.MAIN_ID)
        if (runCatching { ref.get().await().exists() }.getOrDefault(false)) return
        ref.set(
            mapOf(
                "type" to "main",
                "createdAt" to FieldValue.serverTimestamp()
            ),
            SetOptions.merge()
        ).voidAwait()
    }

    /** Creates the per-question conversation if needed and returns its id. */
    suspend fun ensureQuestionConversation(coupleId: String, questionId: String): String {
        val convId = questionConversationId(questionId)
        val ref = conversationsRef(coupleId).document(convId)
        if (!runCatching { ref.get().await().exists() }.getOrDefault(false)) {
            ref.set(
                mapOf(
                    "type" to "question",
                    "questionId" to questionId,
                    "createdAt" to FieldValue.serverTimestamp()
                ),
                SetOptions.merge()
            ).voidAwait()
        }
        return convId
    }

    suspend fun markRead(coupleId: String, conversationId: String, userId: String) {
        conversationsRef(coupleId).document(conversationId)
            .set(mapOf("reads" to mapOf(userId to FieldValue.serverTimestamp())), SetOptions.merge())
            .voidAwait()
    }

    // ─── Messages ──────────────────────────────────────────────────────────────────

    suspend fun sendMessage(coupleId: String, conversationId: String, userId: String, text: String) {
        val aead = encryptionManager.requireAead(coupleId)
        val cipher = fieldEncryptor.encrypt(text, aead, coupleId)
        messagesRef(coupleId, conversationId).add(
            mapOf(
                "authorUserId" to userId,
                "type" to "text",
                "text" to cipher,
                "createdAt" to FieldValue.serverTimestamp()
            )
        ).refAwait()
        updateLastMessage(coupleId, conversationId, userId, cipher)
    }

    suspend fun sendImageMessage(coupleId: String, conversationId: String, userId: String, imageBytes: ByteArray) {
        val aead = encryptionManager.requireAead(coupleId)
        val encrypted = aead.encrypt(imageBytes, coupleId.toByteArray(Charsets.UTF_8))
        val url = storageDataSource.uploadEncryptedMedia(userId, encrypted)
        messagesRef(coupleId, conversationId).add(
            mapOf(
                "authorUserId" to userId,
                "type" to "image",
                "mediaUrl" to url,
                "createdAt" to FieldValue.serverTimestamp()
            )
        ).refAwait()
        // Preview a photo with a fixed (still encrypted) label so the inbox decrypt path is uniform.
        updateLastMessage(coupleId, conversationId, userId, fieldEncryptor.encrypt("📷 Photo", aead, coupleId))
    }

    suspend fun sendVoiceMessage(coupleId: String, conversationId: String, userId: String, audioBytes: ByteArray, durationMs: Long) {
        val aead = encryptionManager.requireAead(coupleId)
        val encrypted = aead.encrypt(audioBytes, coupleId.toByteArray(Charsets.UTF_8))
        val url = storageDataSource.uploadEncryptedMedia(userId, encrypted)
        messagesRef(coupleId, conversationId).add(
            mapOf(
                "authorUserId" to userId,
                "type" to "voice",
                "mediaUrl" to url,
                "durationMs" to durationMs,
                "createdAt" to FieldValue.serverTimestamp()
            )
        ).refAwait()
        updateLastMessage(coupleId, conversationId, userId, fieldEncryptor.encrypt("🎤 Voice message", aead, coupleId))
    }

    private suspend fun updateLastMessage(coupleId: String, conversationId: String, userId: String, encryptedPreview: String) {
        conversationsRef(coupleId).document(conversationId).set(
            mapOf(
                "lastMessageAt" to FieldValue.serverTimestamp(),
                "lastMessagePreview" to encryptedPreview,
                "lastMessageSenderId" to userId
            ),
            SetOptions.merge()
        ).voidAwait()
    }

    /**
     * Live listener on the newest [limit] messages (ascending for display). Using `limitToLast`
     * keeps just-sent messages inside the window regardless of pending server-timestamp ordering;
     * "load older" simply grows [limit] (a single live listener, so incoming messages still appear).
     */
    fun observeMessages(coupleId: String, conversationId: String, limit: Int): Flow<List<QuestionMessage>> = callbackFlow {
        val listener = messagesRef(coupleId, conversationId)
            .orderBy("createdAt", Query.Direction.ASCENDING)
            .limitToLast(limit.toLong())
            .addSnapshotListener { snap, err ->
                if (err != null || snap == null) return@addSnapshotListener
                val aead = encryptionManager.aeadFor(coupleId)
                trySend(snap.documents.mapNotNull { it.toQuestionMessage(aead, coupleId) })
            }
        awaitClose { listener.remove() }
    }

    /** Toggles the caller's emoji reaction on a message (pass null to remove). Any couple member may react. */
    suspend fun setReaction(coupleId: String, conversationId: String, messageId: String, userId: String, emoji: String?) {
        val ref = messagesRef(coupleId, conversationId).document(messageId)
        if (emoji == null) {
            ref.update("reactions.$userId", FieldValue.delete()).voidAwait()
        } else {
            ref.update(mapOf("reactions.$userId" to emoji)).voidAwait()
        }
    }

    /** Unsend: tombstones a message (author-only) and reflects it in the inbox preview if it was last. */
    suspend fun deleteMessage(coupleId: String, conversationId: String, messageId: String) {
        messagesRef(coupleId, conversationId).document(messageId).update("deleted", true).voidAwait()
        val latest = runCatching {
            messagesRef(coupleId, conversationId)
                .orderBy("createdAt", Query.Direction.DESCENDING).limit(1).get().await()
        }.getOrNull()
        if (latest?.documents?.firstOrNull()?.id == messageId) {
            val aead = encryptionManager.aeadFor(coupleId) ?: return
            conversationsRef(coupleId).document(conversationId).set(
                mapOf("lastMessagePreview" to fieldEncryptor.encrypt("Message deleted", aead, coupleId)),
                SetOptions.merge()
            ).voidAwait()
        }
    }

    /** Fire-and-forget typing flag on the conversation doc (ephemeral; cleared on stop/send/leave). */
    fun setTyping(coupleId: String, conversationId: String, userId: String, typing: Boolean) {
        val ref = conversationsRef(coupleId).document(conversationId)
        val value = if (typing) FieldValue.serverTimestamp() else FieldValue.delete()
        ref.update(mapOf("typing.$userId" to value)).addOnFailureListener { /* best-effort */ }
    }

    /** Emits the partner's latest typing timestamp (0 if not typing). */
    fun observePartnerTyping(coupleId: String, conversationId: String, currentUserId: String): Flow<Long> = callbackFlow {
        val listener = conversationsRef(coupleId).document(conversationId)
            .addSnapshotListener { snap, err ->
                if (err != null || snap == null) return@addSnapshotListener
                @Suppress("UNCHECKED_CAST")
                val typing = (snap.get("typing") as? Map<String, com.google.firebase.Timestamp>).orEmpty()
                val at = typing.filterKeys { it != currentUserId }.values.maxOfOrNull { it.toDate().time } ?: 0L
                trySend(at)
            }
        awaitClose { listener.remove() }
    }

    /** Emits the partner's most recent read timestamp for this conversation (0 if never read). */
    fun observePartnerReadAt(coupleId: String, conversationId: String, currentUserId: String): Flow<Long> = callbackFlow {
        val listener = conversationsRef(coupleId).document(conversationId)
            .addSnapshotListener { snap, err ->
                if (err != null || snap == null) return@addSnapshotListener
                @Suppress("UNCHECKED_CAST")
                val reads = (snap.get("reads") as? Map<String, com.google.firebase.Timestamp>).orEmpty()
                val partnerReadAt = reads.filterKeys { it != currentUserId }
                    .values.maxOfOrNull { it.toDate().time } ?: 0L
                trySend(partnerReadAt)
            }
        awaitClose { listener.remove() }
    }

    suspend fun loadDecryptedMedia(coupleId: String, mediaUrl: String): ByteArray? {
        val aead = encryptionManager.aeadFor(coupleId) ?: return null
        val cipher = runCatching { storageDataSource.downloadBytes(mediaUrl) }.getOrNull() ?: return null
        return runCatching { aead.decrypt(cipher, coupleId.toByteArray(Charsets.UTF_8)) }.getOrNull()
    }

    // ─── Mappers / await helpers ────────────────────────────────────────────────────

    private fun DocumentSnapshot.toConversation(
        aead: com.google.crypto.tink.Aead?,
        coupleId: String,
        currentUserId: String
    ): Conversation {
        val lastAt = getTimestamp("lastMessageAt")?.toDate()?.time ?: 0L
        val senderId = getString("lastMessageSenderId") ?: ""
        @Suppress("UNCHECKED_CAST")
        val reads = (get("reads") as? Map<String, com.google.firebase.Timestamp>).orEmpty()
        val myReadAt = reads[currentUserId]?.toDate()?.time ?: 0L
        val unread = lastAt > 0L && senderId != currentUserId && lastAt > myReadAt
        return Conversation(
            id = id,
            type = getString("type") ?: "main",
            questionId = getString("questionId") ?: "",
            lastMessagePreview = fieldEncryptor.decryptForDisplay(getString("lastMessagePreview"), aead, coupleId) ?: "",
            lastMessageAt = lastAt,
            lastMessageSenderId = senderId,
            unread = unread
        )
    }

    private fun DocumentSnapshot.toQuestionMessage(
        aead: com.google.crypto.tink.Aead?,
        coupleId: String
    ): QuestionMessage? {
        val userId = getString("authorUserId") ?: return null
        val type = getString("type") ?: "text"
        @Suppress("UNCHECKED_CAST")
        val reactions = (get("reactions") as? Map<String, String>).orEmpty()
        return QuestionMessage(
            id = id,
            userId = userId,
            type = type,
            mediaUrl = getString("mediaUrl") ?: "",
            durationMs = getLong("durationMs") ?: 0L,
            reactions = reactions,
            deleted = getBoolean("deleted") ?: false,
            text = if (type == "text") (fieldEncryptor.decryptForDisplay(getString("text"), aead, coupleId) ?: "") else "",
            createdAt = getTimestamp("createdAt")?.toDate()?.time ?: 0L
        )
    }

    private suspend fun <T> com.google.android.gms.tasks.Task<T>.await(): T =
        suspendCancellableCoroutine { cont ->
            addOnSuccessListener { cont.resume(it) }
            addOnFailureListener { cont.resumeWithException(it) }
        }

    private suspend fun com.google.android.gms.tasks.Task<Void>.voidAwait() =
        suspendCancellableCoroutine<Unit> { cont ->
            addOnSuccessListener { cont.resume(Unit) }
            addOnFailureListener { cont.resumeWithException(it) }
        }

    private suspend fun com.google.android.gms.tasks.Task<com.google.firebase.firestore.DocumentReference>.refAwait() =
        suspendCancellableCoroutine<Unit> { cont ->
            addOnSuccessListener { cont.resume(Unit) }
            addOnFailureListener { cont.resumeWithException(it) }
        }
}
